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 + ], + ); } } }