-
Notifications
You must be signed in to change notification settings - Fork 480
/
reduxTest.js
318 lines (272 loc) · 10.2 KB
/
reduxTest.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import sinon from 'sinon';
import {expect} from '../../../../util/configuredChai';
import {
getStore,
registerReducers,
stubRedux,
restoreRedux
} from '@cdo/apps/redux';
import {
reducers,
selectors,
actions
} from '@cdo/apps/lib/tools/jsdebugger/redux';
import CommandHistory from '@cdo/apps/lib/tools/jsdebugger/CommandHistory';
import Observer from '@cdo/apps/Observer';
import JSInterpreter from '@cdo/apps/lib/tools/jsinterpreter/JSInterpreter';
describe('The JSDebugger redux duck', () => {
let store, state, studioApp, interpreter;
beforeEach(() => {
stubRedux();
registerReducers(reducers);
store = getStore();
state = store.getState();
studioApp = {hideSource: true};
interpreter = new JSInterpreter({
shouldRunAtMaxSpeed: () => false,
studioApp
});
sinon.spy(interpreter, 'handlePauseContinue');
sinon.spy(interpreter, 'handleStepIn');
sinon.spy(interpreter, 'handleStepOut');
sinon.spy(interpreter, 'handleStepOver');
// override evalInCurrentScope so we don't have to set up the full interpreter.
sinon
.stub(interpreter, 'evalInCurrentScope')
// eslint-disable-next-line no-eval
.callsFake(input => eval(input));
});
afterEach(() => {
restoreRedux();
});
function runToBreakpoint() {
const code = '0;\n1;\n2;\n3;\n4;\n5;\n6;\n7;';
interpreter.calculateCodeInfo({code});
interpreter.parse({code});
interpreter.paused = true;
interpreter.nextStep = JSInterpreter.StepType.IN;
interpreter.executeInterpreter(true);
interpreter.isBreakpointRow = function(row) {
return row === 3 || row === 5;
};
const observer = new Observer();
let hitBreakpoint = false;
observer.observe(interpreter.onPause, function() {
hitBreakpoint = true;
});
interpreter.paused = false;
for (let i = 0; !hitBreakpoint && i < 100; i++) {
interpreter.executeInterpreter();
}
}
it('exposes state on the jsdebugger key', () => {
expect(store.getState().jsdebugger).to.be.defined;
});
it('the state can be accesed via the getRoot selector', () => {
expect(selectors.getRoot(state)).to.equal(state.jsdebugger);
});
describe('the initial state', () => {
it('is initially not attached to an interpreter', () => {
expect(selectors.isAttached(state)).to.be.false;
});
it('and therefore has no interpreter', () => {
expect(selectors.getJSInterpreter(state)).to.be.null;
});
it('nor any command history', () => {
expect(selectors.getCommandHistory(state)).to.be.null;
});
it('and no log output', () => {
expect(selectors.getLogOutput(state)).to.equal('');
});
it('and is closed', () => {
expect(selectors.isOpen(state)).to.be.false;
});
});
describe('the open and close actions', () => {
beforeEach(() => store.dispatch(actions.open()));
it('will open the debugger', () => {
expect(selectors.isOpen(store.getState())).to.be.true;
});
it('and close the debugger', () => {
store.dispatch(actions.close());
expect(selectors.isOpen(store.getState())).to.be.false;
});
});
describe('the appendLog action', () => {
it('will append strings to the log output', () => {
store.dispatch(actions.appendLog('foo'));
expect(selectors.getLogOutput(store.getState())).to.equal('foo');
});
it('will append rich objects to the log output', () => {
store.dispatch(actions.appendLog({foo: 'bar'}));
expect(selectors.getLogOutput(store.getState())).to.equal(
'{"foo":"bar"}'
);
});
it('will append multiple things to the log output, joined by newlines', () => {
store.dispatch(actions.appendLog({foo: 'bar'}));
store.dispatch(actions.appendLog('hello'));
expect(selectors.getLogOutput(store.getState())).to.equal(
'{"foo":"bar"}\nhello'
);
});
it('will also trigger the open action if the debugger is not already open', () => {
expect(selectors.isOpen(store.getState())).to.be.false;
store.dispatch(actions.appendLog('open sesame'));
expect(selectors.isOpen(store.getState())).to.be.true;
});
});
describe('before being initialized', () => {
it('will throw an error if you try to step in', () => {
expect(() => store.dispatch(actions.stepIn())).to.throw(
'jsdebugger has not been initialized yet'
);
});
});
describe('after being initialized with a bad runApp implementation', () => {
let runApp;
beforeEach(() => {
runApp = sinon.spy();
store.dispatch(actions.initialize({runApp}));
state = store.getState();
});
it('will throw an error when you try to stepIn()', () => {
expect(() => store.dispatch(actions.stepIn())).to.throw(
'runApp should have attached an interpreter'
);
expect(runApp).to.have.been.called;
});
});
describe('after being initialized', () => {
let runApp;
beforeEach(() => {
runApp = sinon.spy(() => {
store.dispatch(actions.attach(interpreter));
});
store.dispatch(actions.initialize({runApp}));
state = store.getState();
});
it('you can access a command history object', () => {
expect(selectors.getCommandHistory(state)).to.be.an.instanceOf(
CommandHistory
);
});
it('there is no js interpreter attached yet', () => {
expect(selectors.getJSInterpreter(state)).to.be.null;
});
describe('before a js interpreter is attached', () => {
it('the stepOut action throws an error', () => {
expect(() => store.dispatch(actions.stepOut())).to.throw(
'No interpreter has been attached'
);
});
it('the stepOver action throws an error', () => {
expect(() => store.dispatch(actions.stepOver())).to.throw(
'No interpreter has been attached'
);
});
it('the evalInCurrentScope action throws an error', () => {
expect(() =>
store.dispatch(actions.evalInCurrentScope('1+1'))
).to.throw('No interpreter has been attached');
});
describe('after dispatching the stepIn() action', () => {
beforeEach(() => {
store.dispatch(actions.stepIn());
state = store.getState();
});
it('will call whatever runApp function was provided', () => {
expect(runApp).to.have.been.called;
});
it("will immediately call the interpreter's handlePauseContinue method", () => {
expect(
selectors.getJSInterpreter(store.getState()).handlePauseContinue
).to.have.been.called;
});
it("will call the interpreter's handleStepIn() method", () => {
expect(selectors.getJSInterpreter(store.getState()).handleStepIn).to
.have.been.called;
});
});
});
describe('after being attached to an interpreter', () => {
beforeEach(() => {
store.dispatch(actions.attach(interpreter));
state = store.getState();
});
it('you can get the jsinterpreter instance that was attached', () => {
expect(selectors.getJSInterpreter(state)).to.equal(interpreter);
});
it('the interpreter will trigger pause actions on breakpoints', () => {
expect(selectors.isPaused(state)).to.be.false;
runToBreakpoint();
expect(selectors.isPaused(store.getState())).to.be.true;
});
it('the interpreter will open the debugger on breakpoints', () => {
expect(selectors.isOpen(state)).to.be.false;
runToBreakpoint();
expect(selectors.isOpen(store.getState())).to.be.true;
});
it('the interpreter will log execution warnings', () => {
expect(selectors.getLogOutput(state)).to.equal('');
interpreter.onExecutionWarning.notifyObservers('ouch!', 10);
expect(selectors.getLogOutput(store.getState())).to.equal('ouch!');
});
it("changes to the interpreter's next step will be mirrored", () => {
expect(selectors.canRunNext(state)).to.be.false;
runToBreakpoint();
expect(selectors.canRunNext(store.getState())).to.be.true;
});
it('you can dispatch the stepOut action', () => {
store.dispatch(actions.stepOut());
expect(selectors.getJSInterpreter(store.getState()).handleStepOut).to
.have.been.called;
});
it('you can dispatch the stepOver action', () => {
store.dispatch(actions.stepOver());
expect(selectors.getJSInterpreter(store.getState()).handleStepOver).to
.have.been.called;
});
it('you can dispatch the evalInCurrentScope action', () => {
const result = store.dispatch(actions.evalInCurrentScope('1+1'));
expect(selectors.getJSInterpreter(store.getState()).evalInCurrentScope)
.to.have.been.called;
expect(result).to.equal(2);
});
describe('after dispatching the stepIn() action', () => {
beforeEach(() => {
store.dispatch(actions.stepIn());
state = store.getState();
});
it('will not call the provided runApp, because an interpreter is already attached', () => {
expect(runApp).not.to.have.been.called;
});
});
describe('after being detached', () => {
beforeEach(() => {
store.dispatch(actions.detach());
selectors.getJSInterpreter(state).deinitialize();
state = store.getState();
});
it('will no longer have a jsinterpreter', () => {
expect(selectors.getJSInterpreter(state)).to.be.null;
});
it('will no longer trigger pause actions', () => {
expect(selectors.isPaused(state)).to.be.false;
runToBreakpoint();
expect(selectors.isPaused(store.getState())).to.be.false;
});
it('will no longer log execution warnings', () => {
expect(selectors.getLogOutput(state)).to.equal('');
interpreter.onExecutionWarning.notifyObservers('ouch!', 10);
expect(selectors.getLogOutput(store.getState())).to.equal('');
});
it('will no longer mirror changes to the interpreter state', () => {
expect(selectors.canRunNext(state)).to.be.false;
runToBreakpoint();
expect(selectors.canRunNext(store.getState())).to.be.false;
});
});
});
});
});