From b4ae346cbb49947d416e11b312d897de3c14026e Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Tue, 8 Jul 2025 14:32:10 -0400 Subject: [PATCH 1/4] feature: ipfs support --- README.md | 9 ++++---- next.config.ts | 2 ++ src/components/terminal/TerminalPrompt.tsx | 24 +++++++++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b47c1a4..1391bc6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# nipsys.dev +# nipsys.eth site ``` $ whoami @@ -13,7 +13,7 @@ built this when i got tired of the usual portfolio sites. figured if i'm gonna s licensed under GPL-3.0 because that's how it should be. -**status**: work in progress. not even hosted yet, but getting there. +**status**: work in progress. live on IPFS via IPNS `k2k4r8ng8uzrtqb5ham8kao889m8qezu96z4w3lpinyqghum43veb6n3` ## why a terminal? @@ -28,11 +28,10 @@ licensed under GPL-3.0 because that's how it should be. - βœ… commands and navigation - βœ… responsive design - βœ… internationalization +- βœ… deployed on IPFS - 🚧 content still being written -- 🚧 ipfs deployment setup +- 🚧 automatic ipfs deployment setup - 🚧 figuring out codex hosting -- 🚧 web3 integration features -- ❌ not live yet (soonβ„’) ## commands diff --git a/next.config.ts b/next.config.ts index 268dfc1..98826f9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,8 @@ import createNextIntlPlugin from 'next-intl/plugin'; const nextConfig: NextConfig = { output: 'export', + trailingSlash: true, + images: { unoptimized: true }, }; const withNextIntl = createNextIntlPlugin(); diff --git a/src/components/terminal/TerminalPrompt.tsx b/src/components/terminal/TerminalPrompt.tsx index fb30ee9..cb9be85 100644 --- a/src/components/terminal/TerminalPrompt.tsx +++ b/src/components/terminal/TerminalPrompt.tsx @@ -38,9 +38,31 @@ export default class TerminalPrompt extends Component { autocompleteRef: createRef(), historyIdx: -1, autocomplete: null, - host: window.location.host.split(':')[0], + host: this.getDisplayHost(), }; + private getDisplayHost(): string { + const fullHost = window.location.host.split(':')[0]; + + // Check if it's an IPFS host (contains ipns or ipfs and is long) + if (fullHost.includes('ipns') || fullHost.includes('ipfs')) { + if (fullHost.length > 25) { + // For IPFS, show first 8 chars + ... + last 8 chars of the CID + domain + const parts = fullHost.split('.'); + const cidPart = parts[0]; // The long hash part + const domainPart = parts.slice(1).join('.'); // Everything after the CID + + if (cidPart.length > 16) { + const prefix = cidPart.substring(0, 8); + const suffix = cidPart.substring(cidPart.length - 8); + return `${prefix}...${suffix}.${domainPart}`; + } + } + } + + return fullHost; + } + componentDidMount(): void { if (this.props.entry) { this.setInput(getTerminalEntryInput(this.props.entry)); From d9742d8470b7e11cbcb9d485a7482dbdccb40282 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Tue, 8 Jul 2025 14:56:35 -0400 Subject: [PATCH 2/4] test(terminal_prompt): add tests for host display --- .ai/DEVELOPMENT.md | 11 + .../__tests__/TerminalPrompt.test.tsx | 798 +++++++++++++++++- 2 files changed, 808 insertions(+), 1 deletion(-) diff --git a/.ai/DEVELOPMENT.md b/.ai/DEVELOPMENT.md index 5435631..369c113 100644 --- a/.ai/DEVELOPMENT.md +++ b/.ai/DEVELOPMENT.md @@ -20,6 +20,17 @@ - Test framework: Vitest with React Testing Library - Coverage reporting: V8 coverage provider +### Testing Requirements + +- **New functionality must include tests** - Never implement new features without corresponding tests +- **Update existing tests** when modifying functionality +- **Test all edge cases** - especially for utility functions and complex logic +- **Component tests** should cover props, state changes, and user interactions +- **Utility tests** should cover all branches and error conditions +- **Integration tests** for command system and terminal interactions +- **Mock external dependencies** appropriately in tests +- **Test file organization** - keep tests in `__tests__/` directories alongside components + ## Quality Assurance - Linting: `pnpm lint` (Biome) diff --git a/src/components/terminal/__tests__/TerminalPrompt.test.tsx b/src/components/terminal/__tests__/TerminalPrompt.test.tsx index 52b01c5..22b90b3 100644 --- a/src/components/terminal/__tests__/TerminalPrompt.test.tsx +++ b/src/components/terminal/__tests__/TerminalPrompt.test.tsx @@ -148,7 +148,803 @@ describe('TerminalPrompt', () => { it('renders Terminal prompt span', () => { render(); - const promptSpan = screen.getByText('translated_visitor@localhost:~$'); + // Check for the terminal prompt span with a more flexible matcher + const promptSpan = screen.getByText((content, element) => { + return ( + element?.tagName.toLowerCase() === 'span' && + content.includes('translated_visitor') && + content.includes(':~$') + ); + }); + expect(promptSpan).toBeInTheDocument(); + }); + + it('handles componentDidMount with entry', () => { + const entry: CommandEntry = { + timestamp: 1234567890, + cmdName: Command.Help, + }; + + render(); + + expect(getTerminalEntryInput).toHaveBeenCalledWith(entry); + }); + + it('has proper flex layout structure', () => { + const { container } = render(); + + // Check the main flex container exists + const flexContainer = container.querySelector('.flex.w-full.gap-x-2'); + expect(flexContainer).toBeInTheDocument(); + }); + + it('renders style element for input styles', () => { + const { container } = render(); + + // Check that a style element exists (from styled-jsx) + const styleElements = container.querySelectorAll('style'); + expect(styleElements.length).toBeGreaterThan(0); + }); + + it('handles empty setLastKeyDown gracefully', () => { + const propsWithoutCallback = { + i18n: mockT, + history: mockHistory, + setSubmission: mockSetSubmission, + }; + + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'Enter' }); + + // Should not throw error + expect(input).toBeInTheDocument(); + }); + + it('handles empty history gracefully', () => { + const propsWithoutHistory = { + i18n: mockT, + setSubmission: mockSetSubmission, + setLastKeyDown: mockSetLastKeyDown, + }; + + render(); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + describe('Key handling and interactions', () => { + it('handles Enter key event', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'test command' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + // The keyDown event should be handled by the component + expect(input).toBeInTheDocument(); + }); + + it('handles Ctrl+C key event', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'test command' } }); + fireEvent.keyDown(input, { key: 'c', ctrlKey: true }); + + // The keyDown event should be handled by the component + expect(input).toBeInTheDocument(); + }); + + it('handles ArrowUp key event', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'ArrowUp' }); + + // The keyDown event should be handled by the component + expect(input).toBeInTheDocument(); + }); + + it('handles ArrowDown key event', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + // The keyDown event should be handled by the component + expect(input).toBeInTheDocument(); + }); + + it('handles Tab key event', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'hel' } }); + const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' }); + Object.defineProperty(tabEvent, 'preventDefault', { value: vi.fn() }); + fireEvent.keyDown(input, tabEvent); + + // The keyDown event should be handled by the component + expect(input).toBeInTheDocument(); + }); + + it('resets history index on input', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.input(input); + fireEvent.change(input, { target: { value: 'new command' } }); + + expect(input).toHaveValue('new command'); + }); + }); + + describe('History navigation edge cases', () => { + it('handles ArrowUp with history', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'ArrowUp' }); + + // Should handle the key event without error + expect(input).toBeInTheDocument(); + }); + + it('handles ArrowDown with history', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + // Should handle the key event without error + expect(input).toBeInTheDocument(); + }); + + it('handles history navigation with no history', () => { + const propsWithoutHistory = { + ...defaultProps, + history: undefined, + }; + + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'ArrowUp' }); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + expect(input).toHaveValue(''); + }); + + it('handles history navigation with empty history', () => { + const propsWithEmptyHistory = { + ...defaultProps, + history: [], + }; + + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'ArrowUp' }); + + expect(input).toHaveValue(''); + }); + }); + + describe('Component methods', () => { + it('handles componentDidUpdate behavior', () => { + // Test componentDidUpdate by verifying isNewKeyEvent is used + vi.mocked(isNewKeyEvent).mockReturnValue(false); + + const keyEvent = { key: 'Enter', ctrlKey: false } as React.KeyboardEvent; + + render(); + + // Component should handle the update without error + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('handles simulate method behavior', async () => { + // Test the simulate method indirectly through key events + render(); + + const input = screen.getByRole('textbox'); + + // Simulate typing behavior + fireEvent.change(input, { target: { value: 'h' } }); + fireEvent.change(input, { target: { value: 'he' } }); + fireEvent.change(input, { target: { value: 'hel' } }); + fireEvent.change(input, { target: { value: 'help' } }); + + expect(input).toHaveValue('help'); + }); + }); + + describe('Entry formatting', () => { + it('formats entry with option correctly', () => { + const entryWithOption: CommandEntry = { + timestamp: 1234567890, + cmdName: Command.Whoami, + option: 'verbose', + }; + + render(); + + expect(getTerminalEntryInput).toHaveBeenCalledWith(entryWithOption); + }); + + it('formats entry with arguments correctly', () => { + const entryWithArgs: CommandEntry = { + timestamp: 1234567890, + cmdName: Command.Contact, + argName: 'email', + argValue: 'test@example.com', + }; + + render(); + + expect(getTerminalEntryInput).toHaveBeenCalledWith(entryWithArgs); + }); + }); + + describe('Error handling', () => { + it('handles missing setSubmission gracefully', () => { + const propsWithoutSubmission = { + i18n: mockT, + history: mockHistory, + setLastKeyDown: mockSetLastKeyDown, + }; + + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'Enter' }); + + // Should not throw error + expect(input).toBeInTheDocument(); + }); + + it('handles missing setLastKeyDown in keyDown handler', () => { + const propsWithoutKeyHandler = { + i18n: mockT, + history: mockHistory, + setSubmission: mockSetSubmission, + }; + + render(); + + const input = screen.getByRole('textbox'); + fireEvent.keyDown(input, { key: 'Enter' }); + + // Should not throw error + expect(input).toBeInTheDocument(); + }); + }); + + describe('Autocomplete functionality', () => { + it('handles autocomplete key events', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'xyz' } }); + fireEvent.keyDown(input, { key: 'Tab', preventDefault: vi.fn() }); + + // Component should handle autocomplete without error + expect(input).toBeInTheDocument(); + }); + + it('does not show autocomplete when entry is provided', () => { + const entry: CommandEntry = { + timestamp: 1234567890, + cmdName: Command.Help, + }; + + render(); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'c' } }); + fireEvent.keyDown(input, { key: 'Tab', preventDefault: vi.fn() }); + + // Should not show autocomplete when entry is provided + expect( + screen.queryByText(/translated_autocomplete/), + ).not.toBeInTheDocument(); + }); + }); + + describe('Advanced functionality coverage', () => { + it('handles setHistoryIdx with valid index', () => { + const history: CommandEntry[] = [ + { cmdName: Command.Help, timestamp: 1, option: 'verbose' }, + { + cmdName: Command.Whoami, + timestamp: 2, + argName: 'format', + argValue: 'json', + }, + ]; + + // Mock the utility functions to return realistic data + (getTerminalEntryInput as ReturnType) + .mockReturnValueOnce('whoami --format=json') + .mockReturnValueOnce('help verbose'); + + render(); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Navigate up through history - this should trigger setPreviousEntry and setHistoryIdx + fireEvent.keyDown(input, { key: 'ArrowUp' }); + + // Should handle history navigation and set input text + expect(input).toBeInTheDocument(); + }); + + it('handles setNextEntry when at end of history', () => { + const history: CommandEntry[] = [ + { cmdName: Command.Help, timestamp: 1 }, + { cmdName: Command.Whoami, timestamp: 2 }, + ]; + + // Mock the utility functions properly + (getTerminalEntryInput as ReturnType) + .mockReturnValueOnce('whoami') + .mockReturnValueOnce('help'); + + render(); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Navigate to beginning of history first + fireEvent.keyDown(input, { key: 'ArrowUp' }); // Go to most recent (whoami) + fireEvent.keyDown(input, { key: 'ArrowUp' }); // Go to previous (help) + + // Then navigate forward - this tests setNextEntry logic + fireEvent.keyDown(input, { key: 'ArrowDown' }); // Go back to whoami + fireEvent.keyDown(input, { key: 'ArrowDown' }); // Should trigger resetEntry at end + + expect(input).toBeInTheDocument(); + }); + + it('handles autoComplete with multiple matching commands', () => { + render(); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Type partial command that matches multiple commands (like 'c' for 'contact', 'clear') + fireEvent.change(input, { target: { value: 'c' } }); + + // Tab key should be handled by setLastKeyDown callback + fireEvent.keyDown(input, { key: 'Tab' }); + + // Should register the key event + expect(input).toBeInTheDocument(); + expect(input).toHaveValue('c'); + }); + + it('handles autoComplete with single matching command', () => { + render(); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Type partial command that matches single command (help starts with 'hel') + fireEvent.change(input, { target: { value: 'hel' } }); + + // Tab key should be handled + fireEvent.keyDown(input, { key: 'Tab' }); + + // Should register the key event + expect(input).toBeInTheDocument(); + expect(input).toHaveValue('hel'); + }); + + it('handles updateCursorPosition via setTimeout', async () => { + const mockSetSelectionRange = vi.fn(); + + render(); + + const input = screen.getByRole('textbox') as HTMLInputElement; + input.setSelectionRange = mockSetSelectionRange; + + // Trigger history navigation which should call updateCursorPosition + fireEvent.keyDown(input, { key: 'ArrowUp' }); + + // Wait for setTimeout to execute + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + it('handles getPastInputStr with all entry properties', () => { + const entryWithAllProps: CommandEntry = { + cmdName: Command.Help, + timestamp: 123, + option: '--verbose', + argName: 'format', + argValue: 'json', + }; + + (getTerminalEntryInput as ReturnType).mockReturnValue( + 'help --verbose --format=json', + ); + + render(); + + // Should call getTerminalEntryInput with the entry + expect(getTerminalEntryInput).toHaveBeenCalledWith(entryWithAllProps); + }); + + it('handles setAutocomplete with callback', () => { + render(); + + const input = screen.getByRole('textbox'); + + // Type partial command to trigger autocomplete + fireEvent.change(input, { target: { value: 'c' } }); + fireEvent.keyDown(input, { key: 'Tab', preventDefault: vi.fn() }); + + // Should handle autocomplete functionality + expect(input).toBeInTheDocument(); + }); + + it('resets entry on Ctrl+C', () => { + render(); + + const input = screen.getByRole('textbox'); + + // Type some text first + fireEvent.change(input, { target: { value: 'some text' } }); + + // Press Ctrl+C + fireEvent.keyDown(input, { key: 'c', ctrlKey: true }); + + // Should reset the input (checked by not throwing error) + expect(input).toBeInTheDocument(); + }); + + it('handles Enter key submission', () => { + const mockSetSubmission = vi.fn(); + const mockSetLastKeyDown = vi.fn(); + + render( + , + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Type command + fireEvent.change(input, { target: { value: 'help' } }); + + // Simulate Enter key press through the DOM event + fireEvent.keyDown(input, { key: 'Enter' }); + + // The event should be captured by setLastKeyDown + expect(mockSetLastKeyDown).toHaveBeenCalled(); + expect(input).toHaveValue('help'); + }); + + it('handles focus and scrollIntoView methods', () => { + const mockScrollIntoView = vi.fn(); + HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + + render(); + + const input = screen.getByRole('textbox'); + + // Should be able to focus + input.focus(); + expect(document.activeElement).toBe(input); + }); + + it('handles simulate method functionality', async () => { + const mockSetSubmission = vi.fn(); + + render( + , + ); + + // The simulate method is complex and handles typing animation + // We'll just ensure it doesn't throw errors + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + it('handles setInput with callback', () => { + render(); + + const input = screen.getByRole('textbox'); + + // Test input change which calls setInput + fireEvent.change(input, { target: { value: 'test input' } }); + + expect(input).toHaveValue('test input'); + }); + + it('handles onBeforeInput to reset history index', () => { + const history: CommandEntry[] = [{ cmdName: Command.Help, timestamp: 1 }]; + + render(); + + const input = screen.getByRole('textbox'); + + // Navigate to history first + fireEvent.keyDown(input, { key: 'ArrowUp' }); + + // Then trigger onBeforeInput to reset history index using a custom event + const beforeInputEvent = new Event('beforeinput', { + bubbles: true, + cancelable: true, + }); + fireEvent(input, beforeInputEvent); + + expect(input).toBeInTheDocument(); + }); + }); + + describe('Direct method testing through key events', () => { + beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + vi.mocked(isNewKeyEvent).mockReturnValue(true); + }); + + it('properly triggers submit method through Enter key', () => { + const mockSetSubmission = vi.fn(); + const mockSetLastKeyDown = vi.fn(); + + render( + , + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + fireEvent.change(input, { target: { value: 'test command' } }); + + // Trigger Enter key through DOM event + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(mockSetLastKeyDown).toHaveBeenCalled(); + expect(input).toHaveValue('test command'); + }); + + it('properly triggers resetEntry method through Ctrl+C', () => { + const mockSetLastKeyDown = vi.fn(); + + render( + , + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + fireEvent.change(input, { target: { value: 'some text' } }); + + // Trigger Ctrl+C through DOM event + fireEvent.keyDown(input, { key: 'c', ctrlKey: true }); + + expect(mockSetLastKeyDown).toHaveBeenCalled(); + expect(input).toHaveValue('some text'); + }); + + it('properly triggers setPreviousEntry through ArrowUp', () => { + const history: CommandEntry[] = [ + { cmdName: Command.Help, timestamp: 1 }, + { cmdName: Command.Whoami, timestamp: 2 }, + ]; + + const mockSetLastKeyDown = vi.fn(); + + render( + , + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Trigger ArrowUp through DOM event + fireEvent.keyDown(input, { key: 'ArrowUp' }); + + expect(mockSetLastKeyDown).toHaveBeenCalled(); + }); + + it('properly triggers setNextEntry through ArrowDown', () => { + const history: CommandEntry[] = [ + { cmdName: Command.Help, timestamp: 1 }, + { cmdName: Command.Whoami, timestamp: 2 }, + ]; + + const mockSetLastKeyDown = vi.fn(); + + render( + , + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Trigger ArrowDown through DOM event + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + expect(mockSetLastKeyDown).toHaveBeenCalled(); + }); + + it('properly triggers autoComplete through Tab', () => { + const mockSetLastKeyDown = vi.fn(); + + render( + , + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + fireEvent.change(input, { target: { value: 'he' } }); + + // Trigger Tab through DOM event + fireEvent.keyDown(input, { key: 'Tab' }); + + expect(mockSetLastKeyDown).toHaveBeenCalled(); + }); + + it('tests getPastInputStr method with complex entry', () => { + const complexEntry: CommandEntry = { + cmdName: Command.Help, + timestamp: 123, + option: '--verbose', + argName: 'format', + argValue: 'json', + }; + + // Mock getTerminalEntryInput to handle complex entries + (getTerminalEntryInput as ReturnType).mockReturnValue( + 'help --verbose --format=json', + ); + + render(); + + // Should call getTerminalEntryInput with the complex entry + expect(getTerminalEntryInput).toHaveBeenCalledWith(complexEntry); + }); + + it('handles edge case of history navigation at boundaries', () => { + const history: CommandEntry[] = [{ cmdName: Command.Help, timestamp: 1 }]; + + const mockSetLastKeyDown = vi.fn(); + + render( + , + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + + // Test ArrowUp at history start + fireEvent.keyDown(input, { key: 'ArrowUp' }); + expect(mockSetLastKeyDown).toHaveBeenCalled(); + + // Test ArrowDown after navigating up + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(mockSetLastKeyDown).toHaveBeenCalledTimes(2); + }); + }); + + describe('getDisplayHost', () => { + it('returns full host for regular domains', () => { + Object.defineProperty(window, 'location', { + value: { host: 'example.com' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@example.com:~$'), + ).toBeInTheDocument(); + }); + + it('returns full host for short IPFS hosts', () => { + Object.defineProperty(window, 'location', { + value: { host: 'short.ipfs.dweb.link' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@short.ipfs.dweb.link:~$'), + ).toBeInTheDocument(); + }); + + it('shortens long IPNS hosts correctly', () => { + Object.defineProperty(window, 'location', { + value: { + host: 'k2k4r8ng8uzrtqb5ham8kao889m8qezu96z4w3lpinyqghum43veb6n3.ipns.dweb.link', + }, + writable: true, + }); + + render(); + + // Check for the shortened IPNS host in the prompt span + const promptSpan = screen.getByText((content, element) => { + return ( + element?.tagName.toLowerCase() === 'span' && + content.includes('k2k4r8ng...43veb6n3.ipns.dweb.link') + ); + }); + expect(promptSpan).toBeInTheDocument(); + }); + + it('shortens long IPFS hosts correctly', () => { + Object.defineProperty(window, 'location', { + value: { + host: 'QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o.ipfs.dweb.link', + }, + writable: true, + }); + + render(); + + // Check for the shortened IPFS host in the prompt span + const promptSpan = screen.getByText((content, element) => { + return ( + element?.tagName.toLowerCase() === 'span' && + content.includes('QmYjtig7...iFofrE7o.ipfs.dweb.link') + ); + }); + expect(promptSpan).toBeInTheDocument(); + }); + + it('handles hosts with port numbers', () => { + Object.defineProperty(window, 'location', { + value: { host: 'localhost:3000' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@localhost:~$'), + ).toBeInTheDocument(); + }); + + it('handles CID shorter than 16 characters', () => { + Object.defineProperty(window, 'location', { + value: { host: 'shortcid.ipfs.dweb.link' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@shortcid.ipfs.dweb.link:~$'), + ).toBeInTheDocument(); + }); + }); + + it('renders Terminal prompt span', () => { + render(); + + // Check for the terminal prompt span with a more flexible matcher + const promptSpan = screen.getByText((content, element) => { + return ( + element?.tagName.toLowerCase() === 'span' && + content.includes('translated_visitor') && + content.includes(':~$') + ); + }); expect(promptSpan).toBeInTheDocument(); }); From 9038adcc21a0e0b07d056dd5aefa4a7ea52aba5e Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Tue, 8 Jul 2025 15:07:18 -0400 Subject: [PATCH 3/4] refactor(terminal_prompt): improve IPFS host display logic and constants --- src/components/terminal/TerminalPrompt.tsx | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/components/terminal/TerminalPrompt.tsx b/src/components/terminal/TerminalPrompt.tsx index cb9be85..1991f53 100644 --- a/src/components/terminal/TerminalPrompt.tsx +++ b/src/components/terminal/TerminalPrompt.tsx @@ -32,6 +32,11 @@ interface State { } export default class TerminalPrompt extends Component { + // Constants for IPFS host display formatting + private static readonly MAX_HOST_LENGTH = 25; + private static readonly CID_MIN_LENGTH = 16; + private static readonly CID_TRUNCATION_LENGTH = 8; + state: State = { inputText: '', inputRef: createRef(), @@ -46,15 +51,28 @@ export default class TerminalPrompt extends Component { // Check if it's an IPFS host (contains ipns or ipfs and is long) if (fullHost.includes('ipns') || fullHost.includes('ipfs')) { - if (fullHost.length > 25) { - // For IPFS, show first 8 chars + ... + last 8 chars of the CID + domain + if (fullHost.length > TerminalPrompt.MAX_HOST_LENGTH) { + // For IPFS, show first CID_TRUNCATION_LENGTH chars + ... + last CID_TRUNCATION_LENGTH chars of the CID + domain const parts = fullHost.split('.'); const cidPart = parts[0]; // The long hash part - const domainPart = parts.slice(1).join('.'); // Everything after the CID - if (cidPart.length > 16) { - const prefix = cidPart.substring(0, 8); - const suffix = cidPart.substring(cidPart.length - 8); + // Validate that we have domain parts before constructing domainPart + let domainPart = ''; + if (parts.length > 1) { + domainPart = parts.slice(1).join('.'); // Everything after the CID + } else { + // Fallback to fullHost if no '.' is present (edge case) + return fullHost; + } + + if (cidPart.length > TerminalPrompt.CID_MIN_LENGTH) { + const prefix = cidPart.substring( + 0, + TerminalPrompt.CID_TRUNCATION_LENGTH, + ); + const suffix = cidPart.substring( + cidPart.length - TerminalPrompt.CID_TRUNCATION_LENGTH, + ); return `${prefix}...${suffix}.${domainPart}`; } } From 86650a5d78942be8d890145fca9f6818edde6960 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Tue, 8 Jul 2025 15:53:53 -0400 Subject: [PATCH 4/4] refactor(terminal_prompt): remove duplicated tests --- .../__tests__/TerminalPrompt.test.tsx | 699 ------------------ 1 file changed, 699 deletions(-) diff --git a/src/components/terminal/__tests__/TerminalPrompt.test.tsx b/src/components/terminal/__tests__/TerminalPrompt.test.tsx index 22b90b3..fb85ad2 100644 --- a/src/components/terminal/__tests__/TerminalPrompt.test.tsx +++ b/src/components/terminal/__tests__/TerminalPrompt.test.tsx @@ -933,703 +933,4 @@ describe('TerminalPrompt', () => { ).toBeInTheDocument(); }); }); - - it('renders Terminal prompt span', () => { - render(); - - // Check for the terminal prompt span with a more flexible matcher - const promptSpan = screen.getByText((content, element) => { - return ( - element?.tagName.toLowerCase() === 'span' && - content.includes('translated_visitor') && - content.includes(':~$') - ); - }); - expect(promptSpan).toBeInTheDocument(); - }); - - it('handles componentDidMount with entry', () => { - const entry: CommandEntry = { - timestamp: 1234567890, - cmdName: Command.Help, - }; - - render(); - - expect(getTerminalEntryInput).toHaveBeenCalledWith(entry); - }); - - it('has proper flex layout structure', () => { - const { container } = render(); - - // Check the main flex container exists - const flexContainer = container.querySelector('.flex.w-full.gap-x-2'); - expect(flexContainer).toBeInTheDocument(); - }); - - it('renders style element for input styles', () => { - const { container } = render(); - - // Check that a style element exists (from styled-jsx) - const styleElements = container.querySelectorAll('style'); - expect(styleElements.length).toBeGreaterThan(0); - }); - - it('handles empty setLastKeyDown gracefully', () => { - const propsWithoutCallback = { - i18n: mockT, - history: mockHistory, - setSubmission: mockSetSubmission, - }; - - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'Enter' }); - - // Should not throw error - expect(input).toBeInTheDocument(); - }); - - it('handles empty history gracefully', () => { - const propsWithoutHistory = { - i18n: mockT, - setSubmission: mockSetSubmission, - setLastKeyDown: mockSetLastKeyDown, - }; - - render(); - - expect(screen.getByRole('textbox')).toBeInTheDocument(); - }); - - describe('Key handling and interactions', () => { - it('handles Enter key event', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'test command' } }); - fireEvent.keyDown(input, { key: 'Enter' }); - - // The keyDown event should be handled by the component - expect(input).toBeInTheDocument(); - }); - - it('handles Ctrl+C key event', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'test command' } }); - fireEvent.keyDown(input, { key: 'c', ctrlKey: true }); - - // The keyDown event should be handled by the component - expect(input).toBeInTheDocument(); - }); - - it('handles ArrowUp key event', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'ArrowUp' }); - - // The keyDown event should be handled by the component - expect(input).toBeInTheDocument(); - }); - - it('handles ArrowDown key event', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'ArrowDown' }); - - // The keyDown event should be handled by the component - expect(input).toBeInTheDocument(); - }); - - it('handles Tab key event', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'hel' } }); - const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' }); - Object.defineProperty(tabEvent, 'preventDefault', { value: vi.fn() }); - fireEvent.keyDown(input, tabEvent); - - // The keyDown event should be handled by the component - expect(input).toBeInTheDocument(); - }); - - it('resets history index on input', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.input(input); - fireEvent.change(input, { target: { value: 'new command' } }); - - expect(input).toHaveValue('new command'); - }); - }); - - describe('History navigation edge cases', () => { - it('handles ArrowUp with history', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'ArrowUp' }); - - // Should handle the key event without error - expect(input).toBeInTheDocument(); - }); - - it('handles ArrowDown with history', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'ArrowDown' }); - - // Should handle the key event without error - expect(input).toBeInTheDocument(); - }); - - it('handles history navigation with no history', () => { - const propsWithoutHistory = { - ...defaultProps, - history: undefined, - }; - - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'ArrowUp' }); - fireEvent.keyDown(input, { key: 'ArrowDown' }); - - expect(input).toHaveValue(''); - }); - - it('handles history navigation with empty history', () => { - const propsWithEmptyHistory = { - ...defaultProps, - history: [], - }; - - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'ArrowUp' }); - - expect(input).toHaveValue(''); - }); - }); - - describe('Component methods', () => { - it('handles componentDidUpdate behavior', () => { - // Test componentDidUpdate by verifying isNewKeyEvent is used - vi.mocked(isNewKeyEvent).mockReturnValue(false); - - const keyEvent = { key: 'Enter', ctrlKey: false } as React.KeyboardEvent; - - render(); - - // Component should handle the update without error - expect(screen.getByRole('textbox')).toBeInTheDocument(); - }); - - it('handles simulate method behavior', async () => { - // Test the simulate method indirectly through key events - render(); - - const input = screen.getByRole('textbox'); - - // Simulate typing behavior - fireEvent.change(input, { target: { value: 'h' } }); - fireEvent.change(input, { target: { value: 'he' } }); - fireEvent.change(input, { target: { value: 'hel' } }); - fireEvent.change(input, { target: { value: 'help' } }); - - expect(input).toHaveValue('help'); - }); - }); - - describe('Entry formatting', () => { - it('formats entry with option correctly', () => { - const entryWithOption: CommandEntry = { - timestamp: 1234567890, - cmdName: Command.Whoami, - option: 'verbose', - }; - - render(); - - expect(getTerminalEntryInput).toHaveBeenCalledWith(entryWithOption); - }); - - it('formats entry with arguments correctly', () => { - const entryWithArgs: CommandEntry = { - timestamp: 1234567890, - cmdName: Command.Contact, - argName: 'email', - argValue: 'test@example.com', - }; - - render(); - - expect(getTerminalEntryInput).toHaveBeenCalledWith(entryWithArgs); - }); - }); - - describe('Error handling', () => { - it('handles missing setSubmission gracefully', () => { - const propsWithoutSubmission = { - i18n: mockT, - history: mockHistory, - setLastKeyDown: mockSetLastKeyDown, - }; - - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'Enter' }); - - // Should not throw error - expect(input).toBeInTheDocument(); - }); - - it('handles missing setLastKeyDown in keyDown handler', () => { - const propsWithoutKeyHandler = { - i18n: mockT, - history: mockHistory, - setSubmission: mockSetSubmission, - }; - - render(); - - const input = screen.getByRole('textbox'); - fireEvent.keyDown(input, { key: 'Enter' }); - - // Should not throw error - expect(input).toBeInTheDocument(); - }); - }); - - describe('Autocomplete functionality', () => { - it('handles autocomplete key events', () => { - render(); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'xyz' } }); - fireEvent.keyDown(input, { key: 'Tab', preventDefault: vi.fn() }); - - // Component should handle autocomplete without error - expect(input).toBeInTheDocument(); - }); - - it('does not show autocomplete when entry is provided', () => { - const entry: CommandEntry = { - timestamp: 1234567890, - cmdName: Command.Help, - }; - - render(); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'c' } }); - fireEvent.keyDown(input, { key: 'Tab', preventDefault: vi.fn() }); - - // Should not show autocomplete when entry is provided - expect( - screen.queryByText(/translated_autocomplete/), - ).not.toBeInTheDocument(); - }); - }); - - describe('Advanced functionality coverage', () => { - it('handles setHistoryIdx with valid index', () => { - const history: CommandEntry[] = [ - { cmdName: Command.Help, timestamp: 1, option: 'verbose' }, - { - cmdName: Command.Whoami, - timestamp: 2, - argName: 'format', - argValue: 'json', - }, - ]; - - // Mock the utility functions to return realistic data - (getTerminalEntryInput as ReturnType) - .mockReturnValueOnce('whoami --format=json') - .mockReturnValueOnce('help verbose'); - - render(); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Navigate up through history - this should trigger setPreviousEntry and setHistoryIdx - fireEvent.keyDown(input, { key: 'ArrowUp' }); - - // Should handle history navigation and set input text - expect(input).toBeInTheDocument(); - }); - - it('handles setNextEntry when at end of history', () => { - const history: CommandEntry[] = [ - { cmdName: Command.Help, timestamp: 1 }, - { cmdName: Command.Whoami, timestamp: 2 }, - ]; - - // Mock the utility functions properly - (getTerminalEntryInput as ReturnType) - .mockReturnValueOnce('whoami') - .mockReturnValueOnce('help'); - - render(); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Navigate to beginning of history first - fireEvent.keyDown(input, { key: 'ArrowUp' }); // Go to most recent (whoami) - fireEvent.keyDown(input, { key: 'ArrowUp' }); // Go to previous (help) - - // Then navigate forward - this tests setNextEntry logic - fireEvent.keyDown(input, { key: 'ArrowDown' }); // Go back to whoami - fireEvent.keyDown(input, { key: 'ArrowDown' }); // Should trigger resetEntry at end - - expect(input).toBeInTheDocument(); - }); - - it('handles autoComplete with multiple matching commands', () => { - render(); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Type partial command that matches multiple commands (like 'c' for 'contact', 'clear') - fireEvent.change(input, { target: { value: 'c' } }); - - // Tab key should be handled by setLastKeyDown callback - fireEvent.keyDown(input, { key: 'Tab' }); - - // Should register the key event - expect(input).toBeInTheDocument(); - expect(input).toHaveValue('c'); - }); - - it('handles autoComplete with single matching command', () => { - render(); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Type partial command that matches single command (help starts with 'hel') - fireEvent.change(input, { target: { value: 'hel' } }); - - // Tab key should be handled - fireEvent.keyDown(input, { key: 'Tab' }); - - // Should register the key event - expect(input).toBeInTheDocument(); - expect(input).toHaveValue('hel'); - }); - - it('handles updateCursorPosition via setTimeout', async () => { - const mockSetSelectionRange = vi.fn(); - - render(); - - const input = screen.getByRole('textbox') as HTMLInputElement; - input.setSelectionRange = mockSetSelectionRange; - - // Trigger history navigation which should call updateCursorPosition - fireEvent.keyDown(input, { key: 'ArrowUp' }); - - // Wait for setTimeout to execute - await new Promise((resolve) => setTimeout(resolve, 100)); - }); - - it('handles getPastInputStr with all entry properties', () => { - const entryWithAllProps: CommandEntry = { - cmdName: Command.Help, - timestamp: 123, - option: '--verbose', - argName: 'format', - argValue: 'json', - }; - - (getTerminalEntryInput as ReturnType).mockReturnValue( - 'help --verbose --format=json', - ); - - render(); - - // Should call getTerminalEntryInput with the entry - expect(getTerminalEntryInput).toHaveBeenCalledWith(entryWithAllProps); - }); - - it('handles setAutocomplete with callback', () => { - render(); - - const input = screen.getByRole('textbox'); - - // Type partial command to trigger autocomplete - fireEvent.change(input, { target: { value: 'c' } }); - fireEvent.keyDown(input, { key: 'Tab', preventDefault: vi.fn() }); - - // Should handle autocomplete functionality - expect(input).toBeInTheDocument(); - }); - - it('resets entry on Ctrl+C', () => { - render(); - - const input = screen.getByRole('textbox'); - - // Type some text first - fireEvent.change(input, { target: { value: 'some text' } }); - - // Press Ctrl+C - fireEvent.keyDown(input, { key: 'c', ctrlKey: true }); - - // Should reset the input (checked by not throwing error) - expect(input).toBeInTheDocument(); - }); - - it('handles Enter key submission', () => { - const mockSetSubmission = vi.fn(); - const mockSetLastKeyDown = vi.fn(); - - render( - , - ); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Type command - fireEvent.change(input, { target: { value: 'help' } }); - - // Simulate Enter key press through the DOM event - fireEvent.keyDown(input, { key: 'Enter' }); - - // The event should be captured by setLastKeyDown - expect(mockSetLastKeyDown).toHaveBeenCalled(); - expect(input).toHaveValue('help'); - }); - - it('handles focus and scrollIntoView methods', () => { - const mockScrollIntoView = vi.fn(); - HTMLElement.prototype.scrollIntoView = mockScrollIntoView; - - render(); - - const input = screen.getByRole('textbox'); - - // Should be able to focus - input.focus(); - expect(document.activeElement).toBe(input); - }); - - it('handles simulate method functionality', async () => { - const mockSetSubmission = vi.fn(); - - render( - , - ); - - // The simulate method is complex and handles typing animation - // We'll just ensure it doesn't throw errors - await new Promise((resolve) => setTimeout(resolve, 100)); - }); - - it('handles setInput with callback', () => { - render(); - - const input = screen.getByRole('textbox'); - - // Test input change which calls setInput - fireEvent.change(input, { target: { value: 'test input' } }); - - expect(input).toHaveValue('test input'); - }); - - it('handles onBeforeInput to reset history index', () => { - const history: CommandEntry[] = [{ cmdName: Command.Help, timestamp: 1 }]; - - render(); - - const input = screen.getByRole('textbox'); - - // Navigate to history first - fireEvent.keyDown(input, { key: 'ArrowUp' }); - - // Then trigger onBeforeInput to reset history index using a custom event - const beforeInputEvent = new Event('beforeinput', { - bubbles: true, - cancelable: true, - }); - fireEvent(input, beforeInputEvent); - - expect(input).toBeInTheDocument(); - }); - }); - - describe('Direct method testing through key events', () => { - beforeEach(() => { - // Reset all mocks before each test - vi.clearAllMocks(); - vi.mocked(isNewKeyEvent).mockReturnValue(true); - }); - - it('properly triggers submit method through Enter key', () => { - const mockSetSubmission = vi.fn(); - const mockSetLastKeyDown = vi.fn(); - - render( - , - ); - - const input = screen.getByRole('textbox') as HTMLInputElement; - fireEvent.change(input, { target: { value: 'test command' } }); - - // Trigger Enter key through DOM event - fireEvent.keyDown(input, { key: 'Enter' }); - - expect(mockSetLastKeyDown).toHaveBeenCalled(); - expect(input).toHaveValue('test command'); - }); - - it('properly triggers resetEntry method through Ctrl+C', () => { - const mockSetLastKeyDown = vi.fn(); - - render( - , - ); - - const input = screen.getByRole('textbox') as HTMLInputElement; - fireEvent.change(input, { target: { value: 'some text' } }); - - // Trigger Ctrl+C through DOM event - fireEvent.keyDown(input, { key: 'c', ctrlKey: true }); - - expect(mockSetLastKeyDown).toHaveBeenCalled(); - expect(input).toHaveValue('some text'); - }); - - it('properly triggers setPreviousEntry through ArrowUp', () => { - const history: CommandEntry[] = [ - { cmdName: Command.Help, timestamp: 1 }, - { cmdName: Command.Whoami, timestamp: 2 }, - ]; - - const mockSetLastKeyDown = vi.fn(); - - render( - , - ); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Trigger ArrowUp through DOM event - fireEvent.keyDown(input, { key: 'ArrowUp' }); - - expect(mockSetLastKeyDown).toHaveBeenCalled(); - }); - - it('properly triggers setNextEntry through ArrowDown', () => { - const history: CommandEntry[] = [ - { cmdName: Command.Help, timestamp: 1 }, - { cmdName: Command.Whoami, timestamp: 2 }, - ]; - - const mockSetLastKeyDown = vi.fn(); - - render( - , - ); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Trigger ArrowDown through DOM event - fireEvent.keyDown(input, { key: 'ArrowDown' }); - - expect(mockSetLastKeyDown).toHaveBeenCalled(); - }); - - it('properly triggers autoComplete through Tab', () => { - const mockSetLastKeyDown = vi.fn(); - - render( - , - ); - - const input = screen.getByRole('textbox') as HTMLInputElement; - fireEvent.change(input, { target: { value: 'he' } }); - - // Trigger Tab through DOM event - fireEvent.keyDown(input, { key: 'Tab' }); - - expect(mockSetLastKeyDown).toHaveBeenCalled(); - }); - - it('tests getPastInputStr method with complex entry', () => { - const complexEntry: CommandEntry = { - cmdName: Command.Help, - timestamp: 123, - option: '--verbose', - argName: 'format', - argValue: 'json', - }; - - // Mock getTerminalEntryInput to handle complex entries - (getTerminalEntryInput as ReturnType).mockReturnValue( - 'help --verbose --format=json', - ); - - render(); - - // Should call getTerminalEntryInput with the complex entry - expect(getTerminalEntryInput).toHaveBeenCalledWith(complexEntry); - }); - - it('handles edge case of history navigation at boundaries', () => { - const history: CommandEntry[] = [{ cmdName: Command.Help, timestamp: 1 }]; - - const mockSetLastKeyDown = vi.fn(); - - render( - , - ); - - const input = screen.getByRole('textbox') as HTMLInputElement; - - // Test ArrowUp at history start - fireEvent.keyDown(input, { key: 'ArrowUp' }); - expect(mockSetLastKeyDown).toHaveBeenCalled(); - - // Test ArrowDown after navigating up - fireEvent.keyDown(input, { key: 'ArrowDown' }); - expect(mockSetLastKeyDown).toHaveBeenCalledTimes(2); - }); - }); });