diff --git a/demo/index.tsx b/demo/index.tsx index 79109d8..e0d6ffc 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -5,6 +5,7 @@ import Terminal, { ColorMode, TerminalInput, TerminalOutput } from '../src/index import './style.css'; const TerminalController = (props = {}) => { + const [isPasswordMode, setIsPasswordMode] = useState(false); const [colorMode, setColorMode] = useState(ColorMode.Dark); const [lineData, setLineData] = useState([ Welcome to the React Terminal UI Demo!👋, @@ -12,27 +13,38 @@ const TerminalController = (props = {}) => { The following example commands are provided:, 'view-source' will navigate to the React Terminal UI github source., 'view-react-docs' will navigate to the react docs., + 'login' will show input password with "*" instead of real string, 'clear' will clear the terminal., ]); - function toggleColorMode (e: MouseEvent) { + function toggleColorMode(e: MouseEvent) { e.preventDefault(); setColorMode(colorMode === ColorMode.Light ? ColorMode.Dark : ColorMode.Light); } - function onInput (input: string) { - let ld = [...lineData]; + function onInput(input: string) { + let ld = [...lineData]; + if (isPasswordMode) { + ld.push({'*'.repeat(input.length)}); + ld.push(Your password received successfully); + setIsPasswordMode(false); + setLineData(ld); + } else { ld.push({input}); - if (input.toLocaleLowerCase().trim() === 'view-source') { - window.open('https://github.com/jonmbake/react-terminal-ui', '_blank'); - } else if (input.toLocaleLowerCase().trim() === 'view-react-docs') { - window.open('https://reactjs.org/docs/getting-started.html', '_blank'); - } else if (input.toLocaleLowerCase().trim() === 'clear') { - ld = []; - } else if (input) { - ld.push(Unrecognized command); + if (input.toLocaleLowerCase().trim() === 'view-source') { + window.open('https://github.com/jonmbake/react-terminal-ui', '_blank'); + } else if (input.toLocaleLowerCase().trim() === 'view-react-docs') { + window.open('https://reactjs.org/docs/getting-started.html', '_blank'); + } else if (input.toLocaleLowerCase().trim() === 'clear') { + ld = []; + } else if (input.toLocaleLowerCase().trim() === 'login') { + ld.push(Please enter your password:); + setIsPasswordMode(true); + } else if (input) { + ld.push(Unrecognized command); + } + setLineData(ld); } - setLineData(ld); } const redBtnClick = () => { @@ -56,15 +68,16 @@ const TerminalController = (props = {}) => { return (
- +
- {lineData} diff --git a/src/index.tsx b/src/index.tsx index 2f4248f..9d8bc64 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -28,6 +28,7 @@ export interface Props { children?: ReactNode; onInput?: ((input: string) => void) | null | undefined; startingInputValue?: string; + passwordField?: boolean; redBtnCallback?: () => void; yellowBtnCallback?: () => void; greenBtnCallback?: () => void; @@ -42,6 +43,7 @@ const Terminal = ({ onInput, children, startingInputValue = "", + passwordField = false, redBtnCallback, yellowBtnCallback, greenBtnCallback, @@ -256,7 +258,7 @@ const Terminal = ({ data-terminal-prompt={prompt || "$"} key="terminal-line-prompt" > - {currentLineInput} + {passwordField ? "*".repeat(currentLineInput.length) : currentLineInput} { }) test('Should render prompt', () => { - const { container } = render( '' } />); + const { container } = render( ''} />); expect(container.querySelectorAll('.react-terminal-line')).toHaveLength(1); expect(container.querySelector('.react-terminal-line.react-terminal-active-input[data-terminal-prompt="$"]')).not.toBeNull(); expect(screen.getByPlaceholderText('Terminal Hidden Input')).toBeInTheDocument(); }); test('Should not render prompt if onInput prop is null or not defined', () => { - const { container } = render(Some terminal output); + const { container } = render(Some terminal output); // Still renders output line... expect(container.querySelectorAll('.react-terminal-line')).toHaveLength(1); // ... but not the prompt @@ -30,7 +30,7 @@ describe('Terminal component', () => { test('Should render terminal lines', () => { const { container } = render( - '' }> + ''}> Some terminal input, Some terminal output @@ -44,7 +44,7 @@ describe('Terminal component', () => { test('Input prompt should not scroll into view when component first loads', () => { render( - '' }> + ''}> Some terminal input, Some terminal output @@ -55,7 +55,7 @@ describe('Terminal component', () => { test('Should accept input and scroll into view', () => { const onInput = jest.fn(); - const { rerender } = render(); + const { rerender } = render(); const hiddenInput = screen.getByPlaceholderText('Terminal Hidden Input'); fireEvent.change(hiddenInput, { target: { value: 'a' } }); expect(screen.getByText('a').className).toEqual('react-terminal-line react-terminal-input react-terminal-active-input'); @@ -63,18 +63,18 @@ describe('Terminal component', () => { expect(onInput.mock.calls.length).toEqual(0); fireEvent.keyDown(hiddenInput, { key: 'Enter', code: 'Enter' }); expect(onInput).toHaveBeenCalledWith('a'); - rerender(a) + rerender(a) jest.runAllTimers(); expect(scrollIntoViewFn).toHaveBeenCalledTimes(1); }); test('Should support changing color mode', () => { - const { container } = render( '' }/>); + const { container } = render( ''} />); expect(container.querySelector('.react-terminal-wrapper.react-terminal-light')).not.toBeNull(); }); test('Should focus if onInput is defined', () => { - const { container } = render( '' }/>) + const { container } = render( ''} />) expect(container.ownerDocument.activeElement?.nodeName).toEqual('INPUT'); expect(container.ownerDocument.activeElement?.className).toEqual('terminal-hidden-input'); }); @@ -85,7 +85,7 @@ describe('Terminal component', () => { }); test('Should take starting input value', () => { - render( '' } startingInputValue="cat file.txt " />) + render( ''} startingInputValue="cat file.txt " />) const renderedLine = screen.getByText('cat file.txt'); expect(renderedLine.className).toContain('react-terminal-line'); }); @@ -96,7 +96,48 @@ describe('Terminal component', () => { }); test('Should not render top button panel if null props passed', () => { - const { container } = render( null} />); + const { container } = render( null} />); expect(container.querySelector('.react-terminal-window-buttons')).toBeNull(); }); + + test('renders input as password and handles masked input', () => { + const TerminalWrapper = () => { + const [lines, setLines] = useState([ + Welcome! + ]); + const [isPasswordMode, setIsPasswordMode] = useState(true); + + return ( + { + const newLines = [...lines]; + if (isPasswordMode) { + newLines.push({'*'.repeat(input.length)}); + newLines.push(Password received); + setIsPasswordMode(false); + } else { + newLines.push({input}); + } + setLines(newLines); + }} + > + {lines} + + ); + }; + + render(); + + const hiddenInput = screen.getByPlaceholderText('Terminal Hidden Input') as HTMLInputElement; + + expect(hiddenInput.type).toBe('password'); + + fireEvent.change(hiddenInput, { target: { value: 'mypassword' } }); + expect(hiddenInput.value).toBe('mypassword'); + + fireEvent.keyDown(hiddenInput, { key: 'Enter', code: 'Enter' }); + + expect(screen.getByText('Password received')).toBeInTheDocument(); + }); });