Skip to content

Commit 7982d5f

Browse files
authored
feat(app): allow app update pop-up notifications to be disabled (#6715)
Closes #6684
1 parent 9432111 commit 7982d5f

26 files changed

+737
-136
lines changed
Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,52 @@
11
// @flow
2+
import * as Config from '../../config'
23
import * as Actions from '../actions'
34

4-
import type { AlertId, AlertsAction } from '../types'
5+
import type { AlertId } from '../types'
56

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

8-
type ActionSpec = {|
9-
should: string,
10-
creator: (...args: Array<any>) => AlertsAction,
11-
args: Array<mixed>,
12-
expected: AlertsAction,
13-
|}
14-
15-
const SPECS: Array<ActionSpec> = [
16-
{
17-
should: 'allow an alert to be triggered',
18-
creator: Actions.alertTriggered,
19-
args: [MOCK_ALERT_ID],
20-
expected: {
9+
describe('alerts actions', () => {
10+
it('should allow an alert to be triggered', () => {
11+
const result = Actions.alertTriggered(MOCK_ALERT_ID)
12+
13+
expect(result).toEqual({
2114
type: 'alerts:ALERT_TRIGGERED',
2215
payload: { alertId: MOCK_ALERT_ID },
23-
},
24-
},
25-
{
26-
should: 'allow an alert to be dismissed temporarily',
27-
creator: Actions.alertDismissed,
28-
args: [MOCK_ALERT_ID],
29-
expected: {
16+
})
17+
})
18+
19+
it('should allow an alert to be dismissed temporarily', () => {
20+
const result = Actions.alertDismissed(MOCK_ALERT_ID)
21+
22+
expect(result).toEqual({
3023
type: 'alerts:ALERT_DISMISSED',
3124
payload: { alertId: MOCK_ALERT_ID, remember: false },
32-
},
33-
},
34-
{
35-
should: 'allow an alert to be dismissed permanently',
36-
creator: Actions.alertDismissed,
37-
args: [MOCK_ALERT_ID, true],
38-
expected: {
25+
})
26+
})
27+
28+
it('should allow an alert to be dismissed permanently', () => {
29+
const result = Actions.alertDismissed(MOCK_ALERT_ID, true)
30+
31+
expect(result).toEqual({
3932
type: 'alerts:ALERT_DISMISSED',
4033
payload: { alertId: MOCK_ALERT_ID, remember: true },
41-
},
42-
},
43-
]
44-
45-
describe('alerts actions', () => {
46-
SPECS.forEach(({ should, creator, args, expected }) => {
47-
it(`should ${should}`, () => {
48-
expect(creator).toEqual(expect.any(Function))
49-
expect(creator(...args)).toEqual(expected)
5034
})
5135
})
36+
37+
it('should allow an alert to be ignored permanently', () => {
38+
const result = Actions.alertPermanentlyIgnored(MOCK_ALERT_ID)
39+
40+
expect(result).toEqual(
41+
Config.addUniqueConfigValue('alerts.ignored', MOCK_ALERT_ID)
42+
)
43+
})
44+
45+
it('should allow an alert to be unignored', () => {
46+
const result = Actions.alertUnignored(MOCK_ALERT_ID)
47+
48+
expect(result).toEqual(
49+
Config.subtractConfigValue('alerts.ignored', MOCK_ALERT_ID)
50+
)
51+
})
5252
})

app/src/alerts/__tests__/selectors.test.js

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,88 @@ const MOCK_ALERT_1: AlertId = ('mockAlert1': any)
1515
const MOCK_ALERT_2: AlertId = ('mockAlert2': any)
1616
const MOCK_IGNORED_ALERT: AlertId = ('mockIgnoredAlert': any)
1717

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

21-
beforeEach(() => {
22+
describe('alerts selectors', () => {
23+
const stubGetConfig = (state: State, value = MOCK_CONFIG) => {
2224
getConfig.mockImplementation(s => {
2325
expect(s).toEqual(state)
24-
return { alerts: { ignored: [MOCK_IGNORED_ALERT] } }
26+
return value
2527
})
28+
}
29+
30+
afterEach(() => {
31+
jest.resetAllMocks()
2632
})
2733

2834
it('should be able to get a list of active alerts', () => {
29-
state = ({
35+
const state = ({
3036
alerts: { active: [MOCK_ALERT_1, MOCK_ALERT_2], ignored: [] },
3137
}: $Shape<State>)
38+
39+
stubGetConfig(state)
40+
3241
expect(Selectors.getActiveAlerts(state)).toEqual([
3342
MOCK_ALERT_1,
3443
MOCK_ALERT_2,
3544
])
3645
})
3746

3847
it('should show no active alerts until config is loaded', () => {
39-
getConfig.mockReturnValue(null)
40-
state = ({
48+
const state = ({
4149
alerts: { active: [MOCK_ALERT_1, MOCK_ALERT_2], ignored: [] },
4250
}: $Shape<State>)
51+
52+
stubGetConfig(state, null)
53+
4354
expect(Selectors.getActiveAlerts(state)).toEqual([])
4455
})
4556

4657
it('should filter ignored alerts from active alerts', () => {
4758
// the reducer should never let this state happen, but let's protect
4859
// against it in the selector, too
49-
state = ({
60+
const state = ({
5061
alerts: { active: [MOCK_ALERT_1, MOCK_ALERT_2], ignored: [MOCK_ALERT_2] },
5162
}: $Shape<State>)
63+
64+
stubGetConfig(state)
65+
5266
expect(Selectors.getActiveAlerts(state)).toEqual([MOCK_ALERT_1])
5367
})
5468

5569
it('should filter perma-ignored alerts from active alerts', () => {
56-
state = ({
70+
const state = ({
5771
alerts: { active: [MOCK_ALERT_1, MOCK_IGNORED_ALERT], ignored: [] },
5872
}: $Shape<State>)
73+
74+
stubGetConfig(state)
75+
5976
expect(Selectors.getActiveAlerts(state)).toEqual([MOCK_ALERT_1])
6077
})
78+
79+
it('should be able to tell you if an alert is perma-ignored', () => {
80+
const state = ({ alerts: { active: [], ignored: [] } }: $Shape<State>)
81+
82+
stubGetConfig(state)
83+
84+
expect(
85+
Selectors.getAlertIsPermanentlyIgnored(state, MOCK_IGNORED_ALERT)
86+
).toBe(true)
87+
88+
expect(Selectors.getAlertIsPermanentlyIgnored(state, MOCK_ALERT_1)).toBe(
89+
false
90+
)
91+
})
92+
93+
it('should return null for getAlertIsPermanentlyIgnored if config not initialized', () => {
94+
const state = ({ alerts: { active: [], ignored: [] } }: $Shape<State>)
95+
96+
stubGetConfig(state, null)
97+
98+
expect(
99+
Selectors.getAlertIsPermanentlyIgnored(state, MOCK_IGNORED_ALERT)
100+
).toBe(null)
101+
})
61102
})

app/src/alerts/actions.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
// @flow
22

3+
import { addUniqueConfigValue, subtractConfigValue } from '../config'
34
import * as Constants from './constants'
45
import * as Types from './types'
56

7+
import type {
8+
AddUniqueConfigValueAction,
9+
SubtractConfigValueAction,
10+
} from '../config/types'
11+
612
export const alertTriggered = (
713
alertId: Types.AlertId
814
): Types.AlertTriggeredAction => ({
@@ -17,3 +23,15 @@ export const alertDismissed = (
1723
type: Constants.ALERT_DISMISSED,
1824
payload: { alertId, remember },
1925
})
26+
27+
export const alertPermanentlyIgnored = (
28+
alertId: Types.AlertId
29+
): AddUniqueConfigValueAction => {
30+
return addUniqueConfigValue(Constants.CONFIG_PATH_ALERTS_IGNORED, alertId)
31+
}
32+
33+
export const alertUnignored = (
34+
alertId: Types.AlertId
35+
): SubtractConfigValueAction => {
36+
return subtractConfigValue(Constants.CONFIG_PATH_ALERTS_IGNORED, alertId)
37+
}

app/src/alerts/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// @flow
22

3+
// config path
4+
export const CONFIG_PATH_ALERTS_IGNORED = 'alerts.ignored'
5+
36
// alert types
47
export const ALERT_U2E_DRIVER_OUTDATED: 'u2eDriverOutdated' =
58
'u2eDriverOutdated'

app/src/alerts/epic.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22
import { filter, map } from 'rxjs/operators'
33

4-
import { addUniqueConfigValue } from '../config'
4+
import { alertPermanentlyIgnored } from './actions'
55
import { ALERT_DISMISSED } from './constants'
66

77
import type { Action, Epic } from '../types'
@@ -14,11 +14,6 @@ export const alertsEpic: Epic = (action$, state$) => {
1414
filter<Action, AlertDismissedAction>(
1515
a => a.type === ALERT_DISMISSED && a.payload.remember
1616
),
17-
map(dismissAction => {
18-
return addUniqueConfigValue(
19-
'alerts.ignored',
20-
dismissAction.payload.alertId
21-
)
22-
})
17+
map(dismiss => alertPermanentlyIgnored(dismiss.payload.alertId))
2318
)
2419
}

app/src/alerts/selectors.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,11 @@ export const getActiveAlerts: (
2727
: []
2828
}
2929
)
30+
31+
export const getAlertIsPermanentlyIgnored = (
32+
state: State,
33+
alertId: AlertId
34+
): boolean | null => {
35+
const permaIgnoreList = getIgnoredAlertsFromConfig(state)
36+
return permaIgnoreList ? permaIgnoreList.includes(alertId) : null
37+
}

app/src/components/Alerts/__tests__/Alerts.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('app-wide Alerts component', () => {
4242

4343
const stubActiveAlerts = alertIds => {
4444
getActiveAlerts.mockImplementation(state => {
45-
expect(state).toBe(MOCK_STATE)
45+
expect(state).toEqual(MOCK_STATE)
4646
return alertIds
4747
})
4848
}
@@ -70,11 +70,11 @@ describe('app-wide Alerts component', () => {
7070
})
7171

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

7676
stubActiveAlerts([AppAlerts.ALERT_U2E_DRIVER_OUTDATED])
77-
wrapper.setProps({})
77+
refresh()
7878
expect(wrapper.exists(U2EDriverOutdatedAlert)).toBe(true)
7979

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

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

9191
stubActiveAlerts([AppAlerts.ALERT_APP_UPDATE_AVAILABLE])
92-
wrapper.setProps({})
92+
refresh()
9393
expect(wrapper.exists(UpdateAppModal)).toBe(true)
9494

9595
wrapper.find(UpdateAppModal).invoke('dismissAlert')(true)

app/src/components/RobotSettings/SelectNetwork/ConnectModal/__tests__/form-state.test.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @flow
22
import * as React from 'react'
3-
import { act } from 'react-dom/test-utils'
43
import { mount } from 'enzyme'
54
import * as Formik from 'formik'
65

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

58-
act(() => {
59-
wrapper.setProps({})
60-
})
57+
wrapper.setProps({})
6158

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

75-
act(() => {
76-
wrapper.setProps({})
77-
})
72+
wrapper.setProps({})
7873

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

89-
act(() => {
90-
wrapper.setProps({})
91-
})
84+
wrapper.setProps({})
9285

9386
expect(setTouched).toHaveBeenCalledTimes(1)
9487
expect(setTouched).toHaveBeenCalledWith(

app/src/components/RobotSettings/UpdateBuildroot/SkipAppUpdateMessage.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// @flow
22
import * as React from 'react'
3-
import { C_BLUE, SPACING_3, Link, Text } from '@opentrons/components'
3+
4+
import {
5+
C_BLUE,
6+
FONT_SIZE_INHERIT,
7+
SPACING_3,
8+
Btn,
9+
Text,
10+
} from '@opentrons/components'
411

512
type SkipAppUpdateMessageProps = {|
613
onClick: () => mixed,
@@ -16,9 +23,9 @@ export function SkipAppUpdateMessage(
1623
return (
1724
<Text paddingLeft={SPACING_3}>
1825
{SKIP_APP_MESSAGE}
19-
<Link href="#" color={C_BLUE} onClick={props.onClick}>
26+
<Btn color={C_BLUE} onClick={props.onClick} fontSize={FONT_SIZE_INHERIT}>
2027
{CLICK_HERE}
21-
</Link>
28+
</Btn>
2229
.
2330
</Text>
2431
)

app/src/components/RobotSettings/__tests__/ControlsCard.test.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe('ControlsCard', () => {
232232
return Calibration.DECK_CAL_STATUS_BAD_CALIBRATION
233233
})
234234

235-
const { wrapper } = render()
235+
const { wrapper, refresh } = render()
236236

237237
expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
238238
'Bad deck calibration detected. Please perform a full deck calibration.'
@@ -241,8 +241,7 @@ describe('ControlsCard', () => {
241241
getDeckCalibrationStatus.mockReturnValue(
242242
Calibration.DECK_CAL_STATUS_SINGULARITY
243243
)
244-
wrapper.setProps({})
245-
wrapper.update()
244+
refresh()
246245

247246
expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
248247
'Bad deck calibration detected. Please perform a full deck calibration.'
@@ -251,8 +250,7 @@ describe('ControlsCard', () => {
251250
getDeckCalibrationStatus.mockReturnValue(
252251
Calibration.DECK_CAL_STATUS_IDENTITY
253252
)
254-
wrapper.setProps({})
255-
wrapper.update()
253+
refresh()
256254

257255
expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
258256
'Please perform a full deck calibration.'

0 commit comments

Comments
 (0)