diff --git a/jest.setup.ts b/jest.setup.ts
index 7b0828b..af95130 100644
--- a/jest.setup.ts
+++ b/jest.setup.ts
@@ -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' }),
+ }),
+);
diff --git a/src/components/CLI/CLI.tsx b/src/components/CLI/CLI.tsx
index 50c1c6c..c4ad381 100644
--- a/src/components/CLI/CLI.tsx
+++ b/src/components/CLI/CLI.tsx
@@ -21,10 +21,15 @@ export default function Cli({ decreaseCommandsLeft }: CliProps) {
inputRef.current?.focus()}
>
{output.map((line, index) => (
-
+
{line}
))}
@@ -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"
/>
diff --git a/src/components/CLI/__tests__/index.test.tsx b/src/components/CLI/__tests__/index.test.tsx
new file mode 100644
index 0000000..7500faa
--- /dev/null
+++ b/src/components/CLI/__tests__/index.test.tsx
@@ -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(
);
+
+ const terminalElement = screen.getByTestId('terminal');
+ const cliInputElement = screen.getByTestId
('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);
+ });
+});
diff --git a/src/components/CLI/hooks/useCli.tsx b/src/components/CLI/hooks/useCli.tsx
index a6cfbb3..ddc8a47 100644
--- a/src/components/CLI/hooks/useCli.tsx
+++ b/src/components/CLI/hooks/useCli.tsx
@@ -18,22 +18,20 @@ export const useCli = (decreaseCommandsLeft: () => void) => {
const terminalRef = useRef(null);
const inputRef = useRef(null);
- const handleCommandWrapper = (e: KeyboardEvent) => {
- 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(() => {
@@ -62,27 +60,36 @@ export const useCli = (decreaseCommandsLeft: () => void) => {
const handleInputChange = (e: ChangeEvent) => {
setCommand(e.target.value);
+ // Save current input when starting to navigate history
+ setTempCommand(e.target.value);
};
const handleKeyDown = (e: KeyboardEvent) => {
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();
@@ -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
+ ],
+ );
}
}
}