Skip to content

Commit

Permalink
DiceDB#28: Enhance command history in CLI (DiceDB#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
prathameshkoshti authored Oct 4, 2024
1 parent a284410 commit 572ca60
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 19 deletions.
7 changes: 7 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
import '@testing-library/jest-dom';

// @ts-expect-error not all properties are required here to mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'success' }),
}),
);
8 changes: 7 additions & 1 deletion src/components/CLI/CLI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ export default function Cli({ decreaseCommandsLeft }: CliProps) {
<div
ref={terminalRef}
className="flex flex-col h-full bg-gray-900 text-white font-mono text-sm overflow-auto top-0 pl-4 pb-2"
data-testid="terminal"
onClick={() => inputRef.current?.focus()}
>
{output.map((line, index) => (
<div key={index} className="text-white p-1">
<div
key={index}
data-testid="terminal-output"
className="text-white p-1"
>
{line}
</div>
))}
Expand All @@ -37,6 +42,7 @@ export default function Cli({ decreaseCommandsLeft }: CliProps) {
value={command}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
data-testid="cli-input"
className="w-full bg-transparent outline-none text-white"
/>
</div>
Expand Down
110 changes: 110 additions & 0 deletions src/components/CLI/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CLI from '../CLI';

const decreaseCommandsLeftMock = jest.fn();

const dummyCommands = [
'set abc 100',
'get abc',
'set xyz 200',
'get xyz',
'set pqr 300',
'get pqr',
];

const setupTest = () => {
const user = userEvent.setup();
const utils = render(<CLI decreaseCommandsLeft={decreaseCommandsLeftMock} />);

const terminalElement = screen.getByTestId('terminal');
const cliInputElement = screen.getByTestId<HTMLInputElement>('cli-input');

const typeMultipleCommands = async () => {
for (const command of dummyCommands) {
await user.type(cliInputElement, `${command}{enter}`);
}
};

return {
terminalElement,
cliInputElement,
user,
typeMultipleCommands,
...utils,
};
};

describe('CLI Component', () => {
it('should focus on terminal element click', async () => {
const { terminalElement, cliInputElement, user } = setupTest();
await user.click(terminalElement);
expect(cliInputElement).toHaveFocus();
});

it('should update values when new value is typed', async () => {
const { cliInputElement, user } = setupTest();

// type a command and check if the value is updated
await user.type(cliInputElement, 'set abc');
expect(cliInputElement.value).toBe('set abc');

await user.type(cliInputElement, '{enter}');
expect(cliInputElement.value).toBe('');
});

it('should throw error when user types blacklisted command', async () => {
const { cliInputElement, user, getByTestId } = setupTest();

await user.type(cliInputElement, 'EXEC{enter}');
const terminalOutputElement = getByTestId('terminal-output');
expect(terminalOutputElement).toHaveTextContent(
"(error) ERR unknown command 'EXEC'",
);
});

it('should navigate through command history', async () => {
const { cliInputElement, user, typeMultipleCommands } = setupTest();

await typeMultipleCommands();
expect(cliInputElement.value).toBe('');

await user.keyboard('[ArrowUp]');
expect(cliInputElement.value).toBe(dummyCommands[dummyCommands.length - 1]);

await user.keyboard('[ArrowUp]');
expect(cliInputElement.value).toBe(dummyCommands[dummyCommands.length - 2]);

await user.keyboard('[ArrowDown]');
await user.keyboard('[ArrowDown]');
expect(cliInputElement.value).toBe('');
});

it('should navigate through command history based on current user input', async () => {
const { cliInputElement, user, typeMultipleCommands } = setupTest();
await typeMultipleCommands();
expect(cliInputElement.value).toBe('');

const newCommand = 'set';
await user.type(cliInputElement, newCommand);
const filteredCommands = dummyCommands.filter((cmd) =>
cmd.startsWith(newCommand),
);

await user.keyboard('[ArrowUp]');
expect(cliInputElement.value).toBe(
filteredCommands[filteredCommands.length - 1],
);

await user.keyboard('[ArrowUp]');
expect(cliInputElement.value).toBe(
filteredCommands[filteredCommands.length - 2],
);

// check whether typed command is accessible
await user.keyboard('[ArrowDown]');
await user.keyboard('[ArrowDown]');
expect(cliInputElement.value).toBe(newCommand);
});
});
47 changes: 29 additions & 18 deletions src/components/CLI/hooks/useCli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,20 @@ export const useCli = (decreaseCommandsLeft: () => void) => {
const terminalRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);

const handleCommandWrapper = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
const commandName = command.trim().split(' ')[0].toUpperCase(); // Extract the command

if (blacklistedCommands.includes(commandName)) {
setOutput((prevOutput) => [
...prevOutput,
`(error) ERR unknown command '${commandName}'`,
]);
} else {
handleCommand({ command, setOutput }); // Execute if not blacklisted
}
const handleCommandWrapper = () => {
const commandName = command.trim().split(' ')[0].toUpperCase(); // Extract the command

setCommand(''); // Clear input
decreaseCommandsLeft(); // Call to update remaining commands
if (blacklistedCommands.includes(commandName)) {
setOutput((prevOutput) => [
...prevOutput,
`(error) ERR unknown command '${commandName}'`,
]);
} else {
handleCommand({ command, setOutput }); // Execute if not blacklisted
}

setCommand(''); // Clear input
decreaseCommandsLeft(); // Call to update remaining commands
};

useEffect(() => {
Expand Down Expand Up @@ -62,27 +60,36 @@ export const useCli = (decreaseCommandsLeft: () => void) => {

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setCommand(e.target.value);
// Save current input when starting to navigate history
setTempCommand(e.target.value);
};

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleCommandWrapper(e);
handleCommandWrapper();
if (command.trim().length !== 0) {
setCommandHistory((prev) => [...prev, command]);
setHistoryIndex(-1);
}
return;
}

const filteredCommandHistory = commandHistory.filter((cmd) => {
return cmd.startsWith(tempCommand);
});

if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
if (historyIndex < filteredCommandHistory.length - 1) {
if (historyIndex === -1) {
// Save current input when starting to navigate history
setTempCommand(command);
}
const newIndex = historyIndex + 1;
setHistoryIndex(newIndex);
setCommand(commandHistory[commandHistory.length - 1 - newIndex]);
setCommand(
filteredCommandHistory[filteredCommandHistory.length - 1 - newIndex],
);
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
Expand All @@ -93,7 +100,11 @@ export const useCli = (decreaseCommandsLeft: () => void) => {
// Restore the saved input when reaching the bottom
setCommand(tempCommand);
} else {
setCommand(commandHistory[commandHistory.length - 1 - newIndex]);
setCommand(
filteredCommandHistory[
filteredCommandHistory.length - 1 - newIndex
],
);
}
}
}
Expand Down

0 comments on commit 572ca60

Please sign in to comment.