Skip to content

Commit ae460a5

Browse files
authored
fix(browser-repl): ensure that input can't be modified or evaluated while operation is in progress (#2162)
1 parent 0600bf3 commit ae460a5

File tree

4 files changed

+139
-6
lines changed

4 files changed

+139
-6
lines changed

packages/browser-repl/src/components/editor.spec.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import sinon from 'sinon';
22
import { expect } from '../../testing/chai';
3-
import { createCommands } from './editor';
3+
import { Editor, createCommands } from './editor';
44
import type { Command } from '@mongodb-js/compass-editor';
5+
import { shallow } from '../../testing/enzyme';
6+
import React from 'react';
57

68
describe('<Editor />', function () {
79
let commandSpies: Parameters<typeof createCommands>[number];
@@ -104,4 +106,36 @@ describe('<Editor />', function () {
104106
expect(commandSpies.onArrowDownOnLastLine).to.not.have.been.called;
105107
});
106108
});
109+
110+
describe('command callback props', function () {
111+
it('does not call callback props that can modify shell input', function () {
112+
const callbackPropSpies = {
113+
onEnter: sinon.spy(),
114+
onArrowUpOnFirstLine: sinon.stub().resolves(false),
115+
onArrowDownOnLastLine: sinon.stub().resolves(false),
116+
onClearCommand: sinon.spy(),
117+
onSigInt: sinon.spy(),
118+
};
119+
120+
const wrapper = shallow(
121+
<Editor {...callbackPropSpies} operationInProgress />
122+
);
123+
124+
wrapper.instance().onEnter();
125+
expect(callbackPropSpies.onEnter).to.not.have.been.called;
126+
127+
wrapper.instance().onArrowUpOnFirstLine();
128+
expect(callbackPropSpies.onArrowUpOnFirstLine).to.not.have.been.called;
129+
130+
wrapper.instance().onArrowDownOnLastLine();
131+
expect(callbackPropSpies.onArrowDownOnLastLine).to.not.have.been.called;
132+
133+
wrapper.instance().onClearCommand();
134+
expect(callbackPropSpies.onClearCommand).to.not.have.been.called;
135+
136+
// Only onSigInt is allowed when operation is in progress
137+
wrapper.instance().onSigInt();
138+
expect(callbackPropSpies.onSigInt).to.have.been.called;
139+
});
140+
});
107141
});

packages/browser-repl/src/components/editor.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,47 @@ export class Editor extends Component<EditorProps> {
168168
() => null
169169
);
170170
};
171-
this.commands = createCommands(this.props);
171+
this.commands = createCommands(this);
172+
}
173+
174+
onEnter() {
175+
// Do not allow commands that can modify / evaluate the input while
176+
// operation is in progress
177+
if (this.props.operationInProgress) {
178+
return;
179+
}
180+
return this.props.onEnter();
181+
}
182+
183+
onArrowDownOnLastLine() {
184+
// Do not allow commands that can modify / evaluate the input while
185+
// operation is in progress
186+
if (this.props.operationInProgress) {
187+
return Promise.resolve(false);
188+
}
189+
return this.props.onArrowDownOnLastLine();
190+
}
191+
192+
onArrowUpOnFirstLine() {
193+
// Do not allow commands that can modify / evaluate the input while
194+
// operation is in progress
195+
if (this.props.operationInProgress) {
196+
return Promise.resolve(false);
197+
}
198+
return this.props.onArrowUpOnFirstLine();
199+
}
200+
201+
onClearCommand() {
202+
// Do not allow commands that can modify / evaluate the input while
203+
// operation is in progress
204+
if (this.props.operationInProgress) {
205+
return;
206+
}
207+
return this.props.onClearCommand();
208+
}
209+
210+
onSigInt() {
211+
return this.props.onSigInt();
172212
}
173213

174214
render(): JSX.Element {

packages/browser-repl/src/components/shell.spec.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ const wait: (ms?: number) => Promise<void> = (ms = 10) => {
1313
return new Promise((resolve) => setTimeout(resolve, ms));
1414
};
1515

16+
const waitFor = async (fn: () => void, timeout = 10_000) => {
17+
const start = Date.now();
18+
// eslint-disable-next-line no-constant-condition
19+
while (true) {
20+
await wait();
21+
try {
22+
fn();
23+
return;
24+
} catch (err) {
25+
if (Date.now() - start >= timeout) {
26+
throw err;
27+
}
28+
}
29+
}
30+
};
31+
1632
describe('<Shell />', function () {
1733
let onOutputChangedSpy;
1834
let onHistoryChangedSpy;
@@ -523,4 +539,32 @@ describe('<Shell />', function () {
523539
);
524540
expect(wrapper.find('Editor').prop('value')).to.eq('db.coll.find({})');
525541
});
542+
543+
it('evaluates initial lines', async function () {
544+
wrapper = mount(
545+
<Shell
546+
runtime={fakeRuntime}
547+
initialEvaluate={['use test', 'db.coll.find({})']}
548+
/>
549+
);
550+
551+
// The `operationInProgress` state should be set to true right away
552+
expect(wrapper.state('operationInProgress')).to.eq(true);
553+
expect(wrapper.state('output')).to.deep.eq([]);
554+
555+
await waitFor(() => {
556+
// Eventually we can see through output state that initial lines were
557+
// evaluated
558+
expect(
559+
wrapper
560+
.state('output')
561+
.filter((line) => {
562+
return line.format === 'input';
563+
})
564+
.map((line) => {
565+
return line.value;
566+
})
567+
).to.deep.eq(['use test', 'db.coll.find({})']);
568+
});
569+
});
526570
});

packages/browser-repl/src/components/shell.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ const noop = (): void => {
144144
/* */
145145
};
146146

147+
const normalizeInitialEvaluate = (initialEvaluate: string | string[]) => {
148+
return (
149+
Array.isArray(initialEvaluate) ? initialEvaluate : [initialEvaluate]
150+
).filter((line) => {
151+
// Filter out empty lines if passed by accident
152+
return !!line;
153+
});
154+
};
155+
156+
const isInitialEvaluateEmpty = (
157+
initialEvaluate?: string | string[] | undefined
158+
) => {
159+
return (
160+
!initialEvaluate || normalizeInitialEvaluate(initialEvaluate).length === 0
161+
);
162+
};
163+
147164
/**
148165
* The browser-repl Shell component
149166
*/
@@ -166,7 +183,7 @@ export class _Shell extends Component<ShellProps, ShellState> {
166183
private onCancelPasswordPrompt: () => void = noop;
167184

168185
readonly state: ShellState = {
169-
operationInProgress: false,
186+
operationInProgress: !isInitialEvaluateEmpty(this.props.initialEvaluate),
170187
output: this.props.initialOutput.slice(-this.props.maxOutputLength),
171188
history: this.props.initialHistory.slice(0, this.props.maxHistoryLength),
172189
passwordPrompt: '',
@@ -178,9 +195,7 @@ export class _Shell extends Component<ShellProps, ShellState> {
178195
// updated one when actually running the lines
179196
let evalLines: string[] = [];
180197
if (this.props.initialEvaluate) {
181-
evalLines = Array.isArray(this.props.initialEvaluate)
182-
? this.props.initialEvaluate
183-
: [this.props.initialEvaluate];
198+
evalLines = normalizeInitialEvaluate(this.props.initialEvaluate);
184199
}
185200
this.scrollToBottom();
186201
void this.updateShellPrompt().then(async () => {

0 commit comments

Comments
 (0)