Skip to content

Commit

Permalink
feat(app): allow app update pop-up notifications to be disabled
Browse files Browse the repository at this point in the history
Closes #6684
  • Loading branch information
mcous committed Oct 9, 2020
1 parent 2187072 commit 3ff8dc8
Show file tree
Hide file tree
Showing 24 changed files with 654 additions and 112 deletions.
74 changes: 37 additions & 37 deletions app/src/alerts/__tests__/actions.test.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
// @flow
import * as Config from '../../config'
import * as Actions from '../actions'

import type { AlertId, AlertsAction } from '../types'
import type { AlertId } from '../types'

const MOCK_ALERT_ID: AlertId = ('mockAlert': any)

type ActionSpec = {|
should: string,
creator: (...args: Array<any>) => AlertsAction,
args: Array<mixed>,
expected: AlertsAction,
|}

const SPECS: Array<ActionSpec> = [
{
should: 'allow an alert to be triggered',
creator: Actions.alertTriggered,
args: [MOCK_ALERT_ID],
expected: {
describe('alerts actions', () => {
it('should allow an alert to be triggered', () => {
const result = Actions.alertTriggered(MOCK_ALERT_ID)

expect(result).toEqual({
type: 'alerts:ALERT_TRIGGERED',
payload: { alertId: MOCK_ALERT_ID },
},
},
{
should: 'allow an alert to be dismissed temporarily',
creator: Actions.alertDismissed,
args: [MOCK_ALERT_ID],
expected: {
})
})

it('should allow an alert to be dismissed temporarily', () => {
const result = Actions.alertDismissed(MOCK_ALERT_ID)

expect(result).toEqual({
type: 'alerts:ALERT_DISMISSED',
payload: { alertId: MOCK_ALERT_ID, remember: false },
},
},
{
should: 'allow an alert to be dismissed permanently',
creator: Actions.alertDismissed,
args: [MOCK_ALERT_ID, true],
expected: {
})
})

it('should allow an alert to be dismissed permanently', () => {
const result = Actions.alertDismissed(MOCK_ALERT_ID, true)

expect(result).toEqual({
type: 'alerts:ALERT_DISMISSED',
payload: { alertId: MOCK_ALERT_ID, remember: true },
},
},
]

describe('alerts actions', () => {
SPECS.forEach(({ should, creator, args, expected }) => {
it(`should ${should}`, () => {
expect(creator).toEqual(expect.any(Function))
expect(creator(...args)).toEqual(expected)
})
})

it('should allow an alert to be ignored permanently', () => {
const result = Actions.alertPermanentlyIgnored(MOCK_ALERT_ID)

expect(result).toEqual(
Config.addUniqueConfigValue('alerts.ignored', MOCK_ALERT_ID)
)
})

it('should allow an alert to be unignored', () => {
const result = Actions.alertUnignored(MOCK_ALERT_ID)

expect(result).toEqual(
Config.subtractConfigValue('alerts.ignored', MOCK_ALERT_ID)
)
})
})
59 changes: 50 additions & 9 deletions app/src/alerts/__tests__/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,88 @@ const MOCK_ALERT_1: AlertId = ('mockAlert1': any)
const MOCK_ALERT_2: AlertId = ('mockAlert2': any)
const MOCK_IGNORED_ALERT: AlertId = ('mockIgnoredAlert': any)

describe('alerts selectors', () => {
let state: State
const MOCK_CONFIG: $Shape<Config> = {
alerts: { ignored: [MOCK_IGNORED_ALERT] },
}

beforeEach(() => {
describe('alerts selectors', () => {
const stubGetConfig = (state: State, value = MOCK_CONFIG) => {
getConfig.mockImplementation(s => {
expect(s).toEqual(state)
return { alerts: { ignored: [MOCK_IGNORED_ALERT] } }
return value
})
}

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

it('should be able to get a list of active alerts', () => {
state = ({
const state = ({
alerts: { active: [MOCK_ALERT_1, MOCK_ALERT_2], ignored: [] },
}: $Shape<State>)

stubGetConfig(state)

expect(Selectors.getActiveAlerts(state)).toEqual([
MOCK_ALERT_1,
MOCK_ALERT_2,
])
})

it('should show no active alerts until config is loaded', () => {
getConfig.mockReturnValue(null)
state = ({
const state = ({
alerts: { active: [MOCK_ALERT_1, MOCK_ALERT_2], ignored: [] },
}: $Shape<State>)

stubGetConfig(state, null)

expect(Selectors.getActiveAlerts(state)).toEqual([])
})

it('should filter ignored alerts from active alerts', () => {
// the reducer should never let this state happen, but let's protect
// against it in the selector, too
state = ({
const state = ({
alerts: { active: [MOCK_ALERT_1, MOCK_ALERT_2], ignored: [MOCK_ALERT_2] },
}: $Shape<State>)

stubGetConfig(state)

expect(Selectors.getActiveAlerts(state)).toEqual([MOCK_ALERT_1])
})

it('should filter perma-ignored alerts from active alerts', () => {
state = ({
const state = ({
alerts: { active: [MOCK_ALERT_1, MOCK_IGNORED_ALERT], ignored: [] },
}: $Shape<State>)

stubGetConfig(state)

expect(Selectors.getActiveAlerts(state)).toEqual([MOCK_ALERT_1])
})

it('should be able to tell you if an alert is perma-ignored', () => {
const state = ({ alerts: { active: [], ignored: [] } }: $Shape<State>)

stubGetConfig(state)

expect(
Selectors.getAlertIsPermanentlyIgnored(state, MOCK_IGNORED_ALERT)
).toBe(true)

expect(Selectors.getAlertIsPermanentlyIgnored(state, MOCK_ALERT_1)).toBe(
false
)
})

it('should return null for getAlertIsPermanentlyIgnored if config not initialized', () => {
const state = ({ alerts: { active: [], ignored: [] } }: $Shape<State>)

stubGetConfig(state, null)

expect(
Selectors.getAlertIsPermanentlyIgnored(state, MOCK_IGNORED_ALERT)
).toBe(null)
})
})
18 changes: 18 additions & 0 deletions app/src/alerts/actions.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// @flow

import { addUniqueConfigValue, subtractConfigValue } from '../config'
import * as Constants from './constants'
import * as Types from './types'

import type {
AddUniqueConfigValueAction,
SubtractConfigValueAction,
} from '../config/types'

export const alertTriggered = (
alertId: Types.AlertId
): Types.AlertTriggeredAction => ({
Expand All @@ -17,3 +23,15 @@ export const alertDismissed = (
type: Constants.ALERT_DISMISSED,
payload: { alertId, remember },
})

export const alertPermanentlyIgnored = (
alertId: Types.AlertId
): AddUniqueConfigValueAction => {
return addUniqueConfigValue(Constants.CONFIG_PATH_ALERTS_IGNORED, alertId)
}

export const alertUnignored = (
alertId: Types.AlertId
): SubtractConfigValueAction => {
return subtractConfigValue(Constants.CONFIG_PATH_ALERTS_IGNORED, alertId)
}
3 changes: 3 additions & 0 deletions app/src/alerts/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// @flow

// config path
export const CONFIG_PATH_ALERTS_IGNORED = 'alerts.ignored'

// alert types
export const ALERT_U2E_DRIVER_OUTDATED: 'u2eDriverOutdated' =
'u2eDriverOutdated'
Expand Down
9 changes: 2 additions & 7 deletions app/src/alerts/epic.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow
import { filter, map } from 'rxjs/operators'

import { addUniqueConfigValue } from '../config'
import { alertPermanentlyIgnored } from './actions'
import { ALERT_DISMISSED } from './constants'

import type { Action, Epic } from '../types'
Expand All @@ -14,11 +14,6 @@ export const alertsEpic: Epic = (action$, state$) => {
filter<Action, AlertDismissedAction>(
a => a.type === ALERT_DISMISSED && a.payload.remember
),
map(dismissAction => {
return addUniqueConfigValue(
'alerts.ignored',
dismissAction.payload.alertId
)
})
map(dismiss => alertPermanentlyIgnored(dismiss.payload.alertId))
)
}
8 changes: 8 additions & 0 deletions app/src/alerts/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ export const getActiveAlerts: (
: []
}
)

export const getAlertIsPermanentlyIgnored = (
state: State,
alertId: AlertId
): boolean | null => {
const permaIgnoreList = getIgnoredAlertsFromConfig(state)
return permaIgnoreList ? permaIgnoreList.includes(alertId) : null
}
10 changes: 5 additions & 5 deletions app/src/components/Alerts/__tests__/Alerts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('app-wide Alerts component', () => {

const stubActiveAlerts = alertIds => {
getActiveAlerts.mockImplementation(state => {
expect(state).toBe(MOCK_STATE)
expect(state).toEqual(MOCK_STATE)
return alertIds
})
}
Expand Down Expand Up @@ -70,11 +70,11 @@ describe('app-wide Alerts component', () => {
})

it('should render a U2EDriverOutdatedAlert if alert is triggered', () => {
const { wrapper, store } = render()
const { wrapper, store, refresh } = render()
expect(wrapper.exists(U2EDriverOutdatedAlert)).toBe(false)

stubActiveAlerts([AppAlerts.ALERT_U2E_DRIVER_OUTDATED])
wrapper.setProps({})
refresh()
expect(wrapper.exists(U2EDriverOutdatedAlert)).toBe(true)

wrapper.find(U2EDriverOutdatedAlert).invoke('dismissAlert')(true)
Expand All @@ -85,11 +85,11 @@ describe('app-wide Alerts component', () => {
})

it('should render an UpdateAppModal if appUpdateAvailable alert is triggered', () => {
const { wrapper, store } = render()
const { wrapper, store, refresh } = render()
expect(wrapper.exists(UpdateAppModal)).toBe(false)

stubActiveAlerts([AppAlerts.ALERT_APP_UPDATE_AVAILABLE])
wrapper.setProps({})
refresh()
expect(wrapper.exists(UpdateAppModal)).toBe(true)

wrapper.find(UpdateAppModal).invoke('dismissAlert')(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @flow
import * as React from 'react'
import { act } from 'react-dom/test-utils'
import { mount } from 'enzyme'
import * as Formik from 'formik'

Expand Down Expand Up @@ -55,9 +54,7 @@ describe('ConnectModal state hooks', () => {
mockFormOnce({ ssid: 'foo', securityType: 'qux', psk: 'baz' })
const wrapper = render()

act(() => {
wrapper.setProps({})
})
wrapper.setProps({})

expect(setValues).toHaveBeenCalledTimes(1)
expect(setValues).toHaveBeenCalledWith({
Expand All @@ -72,9 +69,7 @@ describe('ConnectModal state hooks', () => {
mockFormOnce({ ssid: '', securityType: 'qux', psk: 'baz' }, errors)
const wrapper = render()

act(() => {
wrapper.setProps({})
})
wrapper.setProps({})

expect(setErrors).toHaveBeenCalledTimes(1)
expect(setErrors).toHaveBeenCalledWith({ ssid: 'missing!' })
Expand All @@ -86,9 +81,7 @@ describe('ConnectModal state hooks', () => {
mockFormOnce({ ssid: '', securityType: 'qux', psk: 'baz' }, {}, touched)
const wrapper = render()

act(() => {
wrapper.setProps({})
})
wrapper.setProps({})

expect(setTouched).toHaveBeenCalledTimes(1)
expect(setTouched).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// @flow
import * as React from 'react'
import { C_BLUE, SPACING_3, Link, Text } from '@opentrons/components'

import {
C_BLUE,
FONT_SIZE_INHERIT,
SPACING_3,
Btn,
Text,
} from '@opentrons/components'

type SkipAppUpdateMessageProps = {|
onClick: () => mixed,
Expand All @@ -16,9 +23,9 @@ export function SkipAppUpdateMessage(
return (
<Text paddingLeft={SPACING_3}>
{SKIP_APP_MESSAGE}
<Link href="#" color={C_BLUE} onClick={props.onClick}>
<Btn color={C_BLUE} onClick={props.onClick} fontSize={FONT_SIZE_INHERIT}>
{CLICK_HERE}
</Link>
</Btn>
.
</Text>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ describe('ControlsCard', () => {
return Calibration.DECK_CAL_STATUS_BAD_CALIBRATION
})

const { wrapper } = render()
const { wrapper, refresh } = render()

expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
'Bad deck calibration detected. Please perform a full deck calibration.'
Expand All @@ -241,8 +241,7 @@ describe('ControlsCard', () => {
getDeckCalibrationStatus.mockReturnValue(
Calibration.DECK_CAL_STATUS_SINGULARITY
)
wrapper.setProps({})
wrapper.update()
refresh()

expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
'Bad deck calibration detected. Please perform a full deck calibration.'
Expand All @@ -251,8 +250,7 @@ describe('ControlsCard', () => {
getDeckCalibrationStatus.mockReturnValue(
Calibration.DECK_CAL_STATUS_IDENTITY
)
wrapper.setProps({})
wrapper.update()
refresh()

expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
'Please perform a full deck calibration.'
Expand Down
Loading

0 comments on commit 3ff8dc8

Please sign in to comment.