Skip to content

Commit

Permalink
feat(app): Send intercom event on no-cal-block selected (#6893)
Browse files Browse the repository at this point in the history
When the user selects and saves the trash surface as their tip length
calibration target, send an event to intercom so that support can follow
up about fulfilling a calibration block.

This also brings back the intercom event epics and bindings removed in
 #6781, without the calibration check session end bindings.
  • Loading branch information
sfoster1 authored Nov 2, 2020
1 parent 9f2830a commit 8e7059a
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ describe('AskForCalibrationBlockModal', () => {
useTrash: false,
},
{
it: 'no dispatch when trash is picked but not saved',
it:
'no dispatch (but yes intercom event) when trash is picked but not saved',
save: false,
savedVal: null,
useTrash: true,
Expand All @@ -64,7 +65,8 @@ describe('AskForCalibrationBlockModal', () => {
useTrash: false,
},
{
it: 'dispatches config command when trash is picked and saved',
it:
'dispatches config command and fires interocm event when trash is picked and saved',
save: true,
savedVal: false,
useTrash: true,
Expand Down
160 changes: 160 additions & 0 deletions app/src/support/__tests__/epic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// @flow
// support profile epic test
import { TestScheduler } from 'rxjs/testing'
import { configInitialized } from '../../config'
import * as Profile from '../profile'
import * as Event from '../intercom-event'
import { supportEpic } from '../epic'

import type { Action, State } from '../../types'
import type { Config } from '../../config/types'
import type {
SupportConfig,
SupportProfileUpdate,
IntercomEvent,
} from '../types'

jest.mock('../profile')
jest.mock('../intercom-event')

const makeProfileUpdate: JestMockFn<
[Action, State],
SupportProfileUpdate | null
> = Profile.makeProfileUpdate

const makeIntercomEvent: JestMockFn<[Action, State], IntercomEvent | null> =
Event.makeIntercomEvent

const sendEvent: JestMockFn<[IntercomEvent], void> = Event.sendEvent

const initializeProfile: JestMockFn<[SupportConfig], void> =
Profile.initializeProfile

const updateProfile: JestMockFn<[SupportProfileUpdate], void> =
Profile.updateProfile

const MOCK_ACTION: Action = ({ type: 'MOCK_ACTION' }: any)
const MOCK_PROFILE_STATE: $Shape<{| ...State, config: $Shape<Config> |}> = {
config: {
support: { userId: 'foo', createdAt: 42, name: 'bar', email: null },
},
}

const MOCK_EVENT_STATE: $Shape<{| ...State |}> = {}

describe('support profile epic', () => {
let testScheduler

beforeEach(() => {
makeProfileUpdate.mockReturnValue(null)

testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected)
})
})

afterEach(() => {
jest.resetAllMocks()
})

it('should initialize support profile on config:INITIALIZED', () => {
testScheduler.run(({ hot, expectObservable, flush }) => {
const action$ = hot('-a', {
a: configInitialized(MOCK_PROFILE_STATE.config),
})
const state$ = hot('--')
const result$ = supportEpic(action$, state$)

expectObservable(result$, '--')
flush()

expect(initializeProfile).toHaveBeenCalledWith(
MOCK_PROFILE_STATE.config.support
)
})
})

it('should do nothing with actions that do not map to a profile update', () => {
testScheduler.run(({ hot, expectObservable, flush }) => {
const action$ = hot('-a', { a: MOCK_ACTION })
const state$ = hot('s-', { s: MOCK_PROFILE_STATE })
const result$ = supportEpic(action$, state$)

expectObservable(result$, '--')
flush()

expect(makeProfileUpdate).toHaveBeenCalledWith(
MOCK_ACTION,
MOCK_PROFILE_STATE
)
})
})

it('should call a profile update ', () => {
const profileUpdate = { someProp: 'value' }
makeProfileUpdate.mockReturnValueOnce(profileUpdate)

testScheduler.run(({ hot, expectObservable, flush }) => {
const action$ = hot('-a', { a: MOCK_ACTION })
const state$ = hot('s-', { s: MOCK_PROFILE_STATE })
const result$ = supportEpic(action$, state$)

expectObservable(result$)
flush()

expect(updateProfile).toHaveBeenCalledWith(profileUpdate)
})
})
})

describe('support event epic', () => {
let testScheduler

beforeEach(() => {
makeIntercomEvent.mockReturnValue(null)

testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected)
})
})

afterEach(() => {
jest.resetAllMocks()
})

it('should do nothing with actions that do not map to an event', () => {
testScheduler.run(({ hot, expectObservable, flush }) => {
const action$ = hot('-a', { a: MOCK_ACTION })
const state$ = hot('s-', { s: MOCK_EVENT_STATE })
const result$ = supportEpic(action$, state$)

expectObservable(result$, '--')
flush()

expect(makeIntercomEvent).toHaveBeenCalledWith(
MOCK_ACTION,
MOCK_EVENT_STATE
)
expect(sendEvent).not.toHaveBeenCalled()
})
})

it('should send an event', () => {
const eventPayload = {
eventName: 'completed-robot-calibration-check',
metadata: { someProp: 'value' },
}
makeIntercomEvent.mockReturnValueOnce(eventPayload)

testScheduler.run(({ hot, expectObservable, flush }) => {
const action$ = hot('-a', { a: MOCK_ACTION })
const state$ = hot('s-', { s: MOCK_PROFILE_STATE })
const result$ = supportEpic(action$, state$)

expectObservable(result$)
flush()

expect(sendEvent).toHaveBeenCalledWith(eventPayload)
})
})
})
66 changes: 66 additions & 0 deletions app/src/support/__tests__/intercom-event.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// @flow

import type { IntercomPayload } from '../types'
import type { State } from '../../types'
import * as Binding from '../intercom-binding'
import * as Calibration from '../../calibration'
import * as Config from '../../config'
import { makeIntercomEvent, sendEvent } from '../intercom-event'
import * as Constants from '../constants'

jest.mock('../intercom-binding')
jest.mock('../../sessions/selectors')

const sendIntercomEvent: JestMockFn<[string, IntercomPayload], void> =
Binding.sendIntercomEvent

const MOCK_STATE: $Shape<{| ...State |}> = {}

describe('support event tests', () => {
afterEach(() => {
jest.resetAllMocks()
})

it('makeIntercomEvent should ignore unhandled events', () => {
const built = makeIntercomEvent(
Config.toggleConfigValue('some-random-path'),
MOCK_STATE
)
expect(built).toBeNull()
})

it('makeIntercomEvent should send an event for no cal block selected', () => {
expect(
makeIntercomEvent(
Calibration.setUseTrashSurfaceForTipCal(true),
MOCK_STATE
)
).toEqual({
eventName: Constants.INTERCOM_EVENT_NO_CAL_BLOCK,
metadata: {},
})
})
it('makeIntercomEvent should not send an event for cal block present', () => {
expect(
makeIntercomEvent(
Calibration.setUseTrashSurfaceForTipCal(false),
MOCK_STATE
)
).toBe(null)
})

it('sendEvent should pass on its arguments', () => {
const props = {
eventName: Constants.INTERCOM_EVENT_NO_CAL_BLOCK,
metadata: {
someKey: true,
someOtherKey: 'hi',
},
}
sendEvent(props)
expect(sendIntercomEvent).toHaveBeenCalledWith(
props.eventName,
props.metadata
)
})
})
2 changes: 1 addition & 1 deletion app/src/support/__tests__/system-info-profile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const MOCK_ANALYTICS_PROPS = {
'U2E IPv4 Address': '10.0.0.1',
}

describe('system info support profile updates', () => {
describe('custom labware analytics events', () => {
beforeEach(() => {
getU2EDeviceAnalyticsProps.mockImplementation(state => {
expect(state).toBe(MOCK_STATE)
Expand Down
1 change: 1 addition & 0 deletions app/src/support/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export const PROFILE_FEATURE_FLAG = 'Robot FF'
// supported event names
export const INTERCOM_EVENT_CALCHECK_COMPLETE: 'completed-robot-calibration-check' =
'completed-robot-calibration-check'
export const INTERCOM_EVENT_NO_CAL_BLOCK: 'no-cal-block' = 'no-cal-block'
14 changes: 13 additions & 1 deletion app/src/support/epic.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { tap, filter, withLatestFrom, ignoreElements } from 'rxjs/operators'
import * as Cfg from '../config'
import { initializeProfile, makeProfileUpdate, updateProfile } from './profile'

import { makeIntercomEvent, sendEvent } from './intercom-event'

import type { Epic } from '../types'
import type { ConfigInitializedAction } from '../config/types'

Expand All @@ -28,7 +30,17 @@ const updateProfileEpic: Epic = (action$, state$) => {
)
}

const sendEventEpic: Epic = (action$, state$) => {
return action$.pipe(
withLatestFrom(state$, makeIntercomEvent),
filter(maybeSend => maybeSend !== null),
tap(sendEvent),
ignoreElements()
)
}

export const supportEpic: Epic = combineEpics(
initializeSupportEpic,
updateProfileEpic
updateProfileEpic,
sendEventEpic
)
31 changes: 31 additions & 0 deletions app/src/support/intercom-event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @flow
// functions for sending events to intercom, both for enriching user profiles
// and for triggering contextual support conversations
import type { Action, State } from '../types'
import { sendIntercomEvent } from './intercom-binding'
import type { IntercomEvent } from './types'
import { INTERCOM_EVENT_NO_CAL_BLOCK } from './constants'
import * as Config from '../config'

export function makeIntercomEvent(
action: Action,
state: State
): IntercomEvent | null {
switch (action.type) {
case Config.UPDATE_VALUE: {
const { path, value } = action.payload
if (path !== 'calibration.useTrashSurfaceForTipCal' || value !== true) {
return null
}
return {
eventName: INTERCOM_EVENT_NO_CAL_BLOCK,
metadata: {},
}
}
}
return null
}

export function sendEvent(event: IntercomEvent): void {
sendIntercomEvent(event.eventName, event?.metadata ?? {})
}
9 changes: 7 additions & 2 deletions app/src/support/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

import type { Config } from '../config/types'

import typeof { INTERCOM_EVENT_CALCHECK_COMPLETE } from './constants'
import typeof {
INTERCOM_EVENT_CALCHECK_COMPLETE,
INTERCOM_EVENT_NO_CAL_BLOCK,
} from './constants'

export type IntercomEventName = INTERCOM_EVENT_CALCHECK_COMPLETE
export type IntercomEventName =
| INTERCOM_EVENT_CALCHECK_COMPLETE
| INTERCOM_EVENT_NO_CAL_BLOCK

export type SupportConfig = $PropertyType<Config, 'support'>

Expand Down

0 comments on commit 8e7059a

Please sign in to comment.