Skip to content

Commit eeffafe

Browse files
authored
Refactor console reducer to use handleActions, test with Jest (#1687)
* Migrate console reducer to use handleActions Migrates the console reducer to use `handleActions` rather than hand-rolled reducer logic built around a `switch` statement. Uses action creators as keys (which is something `redux-actions` explicitly affords) to reduce repetition of magic action type constant strings. * Add static file mocks for HTML and SVG imports Per Jest recommendations * Rename existing jest test to conform to standard .test.js convention * First jest test for console reducer * Jest test for consoleValueProduced * Jest test for consoleErrorProduced * Jest test for consoleLogBatchProduced * Jest tests for console entry navigation * Remove deprecated console reducer test
1 parent 60357cb commit eeffafe

File tree

6 files changed

+241
-215
lines changed

6 files changed

+241
-215
lines changed

__mocks__/fileMock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'test-file-stub';

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
clearMocks: true,
99
moduleNameMapper: {
1010
'@factories/(.*)$': '<rootDir>/__factories__/$1',
11+
'\\.(html|svg)': '<rootDir>/__mocks__/fileMock.js',
1112
},
1213
testPathIgnorePatterns: [
1314
'/node_modules/',
File renamed without changes.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import forEach from 'lodash-es/forEach';
2+
import map from 'lodash-es/map';
3+
import reduce from 'lodash-es/reduce';
4+
5+
import reducer from '../console';
6+
7+
import {
8+
consoleErrorProduced,
9+
consoleLogBatchProduced,
10+
consoleValueProduced,
11+
evaluateConsoleEntry,
12+
nextConsoleHistory,
13+
previousConsoleHistory,
14+
consoleInputChanged,
15+
} from '../../actions';
16+
17+
test('evaluateConsoleEntry adds entry to history', () => {
18+
const expression = '1 + 1';
19+
const key = '123';
20+
const {history} = applyActions(
21+
evaluateConsoleEntry(expression, key),
22+
);
23+
expect(history.size).toBe(1);
24+
expect(history.get(key)).toMatchObject({expression});
25+
});
26+
27+
test('consoleValueProduced adds value to existing entry', () => {
28+
const value = 2;
29+
const key = '123';
30+
const {history} = applyActions(
31+
evaluateConsoleEntry('1 + 1', key),
32+
consoleValueProduced(key, value),
33+
);
34+
35+
expect(history.get(key)).toMatchObject({value});
36+
});
37+
38+
test('consoleErrorProduced adds error to existing entry', () => {
39+
const key = '123';
40+
const name = 'NameError';
41+
const message = 'bogus is not defined';
42+
const {history} = applyActions(
43+
evaluateConsoleEntry('1 + bogus', key),
44+
consoleErrorProduced(key, name, message, 123456789),
45+
);
46+
47+
expect(history.get(key).error).toMatchObject({name, message});
48+
});
49+
50+
test('consoleInputChanged updates currentInputValue', () => {
51+
const value = '3';
52+
const {currentInputValue} = applyActions(consoleInputChanged(value));
53+
54+
expect(currentInputValue).toEqual(value);
55+
});
56+
57+
test('consoleLogBatchProduced adds entries to history', () => {
58+
const entries = [
59+
{
60+
value: 'Second console message',
61+
compiledProjectKey: 987654321,
62+
key: '123',
63+
},
64+
{
65+
value: 'A console message',
66+
compiledProjectKey: 123456789,
67+
key: '456',
68+
},
69+
];
70+
const {history} = applyActions(consoleLogBatchProduced(entries));
71+
72+
const keysInCorrectOrder = map(entries, 'key');
73+
expect(Array.from(history.keySeq())).toEqual(keysInCorrectOrder);
74+
forEach(entries, ({key, value, compiledProjectKey}) => {
75+
expect(history.get(key)).toMatchObject({
76+
value,
77+
evaluatedByCompiledProjectKey: compiledProjectKey,
78+
});
79+
});
80+
});
81+
82+
describe('previousConsoleHistory', () => {
83+
it('moves cursor from current input to previous entry', () => {
84+
const secondExpression = 'var bar';
85+
const consoleInput = 'va';
86+
const state = applyActions(
87+
evaluateConsoleEntry('var foo', '1'),
88+
evaluateConsoleEntry(secondExpression, '2'),
89+
consoleInputChanged(consoleInput),
90+
previousConsoleHistory(),
91+
);
92+
93+
expect(state).toMatchObject({
94+
currentInputValue: secondExpression,
95+
nextConsoleEntry: consoleInput,
96+
historyEntryIndex: 1,
97+
});
98+
});
99+
100+
it('moves cursor from previous entry to earlier entry', () => {
101+
const firstExpression = 'var foo';
102+
const consoleInput = 'va';
103+
const state = applyActions(
104+
evaluateConsoleEntry(firstExpression, '1'),
105+
evaluateConsoleEntry('var bar', '2'),
106+
consoleInputChanged(consoleInput),
107+
previousConsoleHistory(),
108+
previousConsoleHistory(),
109+
);
110+
111+
expect(state).toMatchObject({
112+
currentInputValue: firstExpression,
113+
nextConsoleEntry: consoleInput,
114+
historyEntryIndex: 2,
115+
});
116+
});
117+
});
118+
119+
describe('nextConsoleHistory', () => {
120+
it('moves from previous entry to more recent previous entry', () => {
121+
const secondExpression = 'var bar';
122+
const consoleInput = 'va';
123+
const state = applyActions(
124+
evaluateConsoleEntry('var foo', '1'),
125+
evaluateConsoleEntry(secondExpression, '2'),
126+
consoleInputChanged(consoleInput),
127+
previousConsoleHistory(),
128+
previousConsoleHistory(),
129+
nextConsoleHistory(),
130+
);
131+
132+
expect(state).toMatchObject({
133+
historyEntryIndex: 1,
134+
nextConsoleEntry: consoleInput,
135+
currentInputValue: secondExpression,
136+
});
137+
});
138+
139+
it('moves from most recent entry to in-progress expression', () => {
140+
const consoleInput = 'va';
141+
const state = applyActions(
142+
evaluateConsoleEntry('var foo', '1'),
143+
evaluateConsoleEntry('var bar', '2'),
144+
consoleInputChanged(consoleInput),
145+
previousConsoleHistory(),
146+
nextConsoleHistory(),
147+
);
148+
149+
expect(state).toMatchObject({
150+
historyEntryIndex: 0,
151+
currentInputValue: consoleInput,
152+
});
153+
expect(state.nextConsoleEntry).toBeNil();
154+
});
155+
});
156+
157+
function applyActions(...actions) {
158+
return reduce(
159+
actions,
160+
(state, action) => reducer(state, action),
161+
undefined,
162+
);
163+
}

src/reducers/console.js

Lines changed: 76 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
import constant from 'lodash-es/constant';
12
import inRange from 'lodash-es/inRange';
3+
import isNil from 'lodash-es/isNil';
4+
import {handleActions} from 'redux-actions';
5+
26

37
import {ConsoleState, ConsoleEntry, ConsoleError} from '../records';
48

9+
import {
10+
consoleValueProduced,
11+
consoleErrorProduced,
12+
evaluateConsoleEntry,
13+
clearConsoleEntries,
14+
previousConsoleHistory,
15+
nextConsoleHistory,
16+
consoleInputChanged,
17+
consoleLogBatchProduced,
18+
} from '../actions/console';
19+
520
const initialState = new ConsoleState();
621

722
function updateConsoleForHistoryIndex(state, index) {
@@ -17,85 +32,75 @@ function updateConsoleForHistoryIndex(state, index) {
1732

1833
const expression = expressionHistory.get(index);
1934

20-
return state.
35+
const nextState = state.
2136
set('historyEntryIndex', index).
2237
set('currentInputValue', expression);
23-
}
2438

25-
function setNextConsoleEntry(state, historyIndex) {
26-
const firstUp = historyIndex === 1;
39+
if (index === 0) {
40+
return nextState.delete('nextConsoleEntry');
41+
}
2742

28-
if (firstUp && !state.history.isEmpty()) {
29-
return state.set('nextConsoleEntry', state.currentInputValue);
43+
if (isNil(state.nextConsoleEntry) && !state.history.isEmpty()) {
44+
return nextState.set('nextConsoleEntry', state.currentInputValue);
3045
}
3146

32-
return state;
47+
return nextState;
3348
}
3449

35-
export default function console(stateIn, {type, payload, meta}) {
36-
let state = stateIn;
37-
if (state === undefined) {
38-
state = initialState;
39-
}
50+
export default handleActions({
51+
[consoleValueProduced]: (
52+
state,
53+
{payload: {compiledProjectKey, key, value}},
54+
) => state.updateIn(
55+
['history', key],
56+
input => input.set('value', value).
57+
set('evaluatedByCompiledProjectKey', compiledProjectKey),
58+
),
59+
60+
[consoleErrorProduced]: (
61+
state,
62+
{payload: {compiledProjectKey, key, message, name}},
63+
) => state.updateIn(
64+
['history', key],
65+
input => input.set('error', new ConsoleError({name, message})).
66+
set('evaluatedByCompiledProjectKey', compiledProjectKey),
67+
),
4068

41-
switch (type) {
42-
case 'CONSOLE_VALUE_PRODUCED':
43-
return state.updateIn(
44-
['history', payload.key],
45-
input => input.set(
46-
'value',
47-
payload.value,
48-
).set(
49-
'evaluatedByCompiledProjectKey',
50-
payload.compiledProjectKey,
51-
),
52-
);
53-
case 'CONSOLE_ERROR_PRODUCED':
54-
return state.updateIn(
55-
['history', payload.key],
56-
input => input.set(
57-
'error',
58-
new ConsoleError({name: payload.name, message: payload.message}),
59-
).set(
60-
'evaluatedByCompiledProjectKey',
61-
payload.compiledProjectKey,
62-
),
63-
);
64-
case 'EVALUATE_CONSOLE_ENTRY':
65-
return payload.trim(' ') === '' ? state : state.setIn(
66-
['history', meta.key],
67-
new ConsoleEntry({expression: payload}),
68-
).
69+
[evaluateConsoleEntry]: (state, {payload: expression, meta: {key}}) =>
70+
expression.trim() === '' ?
71+
state :
72+
state.setIn(['history', key], new ConsoleEntry({expression})).
6973
delete('currentInputValue').
7074
delete('nextConsoleEntry').
71-
delete('historyEntryIndex');
72-
case 'CLEAR_CONSOLE_ENTRIES':
73-
return initialState;
74-
case 'CONSOLE_INPUT_CHANGED':
75-
return state.set('currentInputValue', payload.value);
76-
case 'CONSOLE_LOG_BATCH_PRODUCED':
77-
return state.update(
78-
'history',
79-
history => history.withMutations((map) => {
80-
payload.entries.forEach(({value, compiledProjectKey, key}) => {
81-
map.set(key, new ConsoleEntry({
82-
value,
83-
evaluatedByCompiledProjectKey: compiledProjectKey,
84-
}));
85-
});
86-
}),
87-
);
88-
case 'PREVIOUS_CONSOLE_HISTORY': {
89-
const historyIndex = state.historyEntryIndex + 1;
90-
91-
return updateConsoleForHistoryIndex(
92-
setNextConsoleEntry(state, historyIndex),
93-
historyIndex,
94-
);
95-
}
96-
case 'NEXT_CONSOLE_HISTORY':
97-
return updateConsoleForHistoryIndex(state, state.historyEntryIndex - 1);
98-
default:
99-
return state;
100-
}
101-
}
75+
delete('historyEntryIndex'),
76+
77+
[clearConsoleEntries]: constant(initialState),
78+
79+
[consoleInputChanged]: (state, {payload: {value}}) =>
80+
state.set('currentInputValue', value),
81+
82+
[consoleLogBatchProduced]: (state, {payload: {entries}}) =>
83+
state.update(
84+
'history',
85+
history => history.withMutations((map) => {
86+
entries.forEach(({value, compiledProjectKey, key}) => {
87+
map.set(key, new ConsoleEntry({
88+
value,
89+
evaluatedByCompiledProjectKey: compiledProjectKey,
90+
}));
91+
});
92+
}),
93+
),
94+
95+
[previousConsoleHistory]: (state) => {
96+
const historyIndex = state.historyEntryIndex + 1;
97+
98+
return updateConsoleForHistoryIndex(state, historyIndex);
99+
},
100+
101+
[nextConsoleHistory]: (state) => {
102+
const historyIndex = state.historyEntryIndex - 1;
103+
return updateConsoleForHistoryIndex(state, historyIndex);
104+
},
105+
}, initialState);
106+

0 commit comments

Comments
 (0)