12 changed files with 597 additions and 386 deletions
@ -1,152 +1,154 @@ |
|||
import { MessageInput } from '@components/PageComponents/Messages/MessageInput.tsx'; |
|||
import { useDevice } from "@core/stores/deviceStore.ts"; |
|||
import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; |
|||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; |
|||
import userEvent from '@testing-library/user-event'; |
|||
|
|||
vi.mock("@core/stores/deviceStore.ts", () => ({ |
|||
useDevice: vi.fn(), |
|||
import { vi, describe, it, expect, beforeEach } from 'vitest'; |
|||
import { MessageInput } from './MessageInput.tsx'; |
|||
import { useDevice } from '@core/stores/deviceStore.ts'; |
|||
import { useMessageStore } from '@core/stores/messageStore.ts'; |
|||
import { debounce } from '@core/utils/debounce.ts'; |
|||
import { Types } from "@meshtastic/core"; |
|||
|
|||
vi.mock('@components/UI/Button.tsx', () => ({ |
|||
Button: vi.fn(({ type, className, children, onClick, onSubmit }) => ( |
|||
<button type={type} className={className} onClick={onClick} onSubmit={onSubmit}> |
|||
{children} |
|||
</button> |
|||
)), |
|||
})); |
|||
|
|||
vi.mock("@core/utils/debounce.ts", () => ({ |
|||
debounce: (fn: () => void) => fn, |
|||
vi.mock('@components/UI/Input.tsx', () => ({ |
|||
Input: vi.fn(({ autoFocus, minLength, name, placeholder, value, onChange }) => ( |
|||
<input |
|||
autoFocus={autoFocus} |
|||
minLength={minLength} |
|||
name={name} |
|||
placeholder={placeholder} |
|||
value={value} |
|||
onChange={onChange} |
|||
/> |
|||
)), |
|||
})); |
|||
|
|||
vi.mock("@components/UI/Button.tsx", () => ({ |
|||
Button: ({ children, ...props }: { children: React.ReactNode }) => <button {...props}>{children}</button> |
|||
vi.mock('@core/stores/deviceStore.ts', () => ({ |
|||
useDevice: vi.fn(), |
|||
})); |
|||
|
|||
vi.mock("@components/UI/Input.tsx", () => ({ |
|||
Input: (props: any) => <input {...props} /> |
|||
vi.mock('@core/stores/messageStore.ts', () => ({ |
|||
useMessageStore: vi.fn(), |
|||
MessageState: { |
|||
Ack: 'ack', |
|||
Waiting: 'waiting', |
|||
Failed: 'failed', |
|||
}, |
|||
MessageType: { |
|||
Direct: 'direct', |
|||
Broadcast: 'broadcast', |
|||
}, |
|||
})); |
|||
|
|||
vi.mock("lucide-react", () => ({ |
|||
SendIcon: () => <div data-testid="send-icon">Send</div> |
|||
vi.mock('@core/utils/debounce.ts', () => ({ |
|||
debounce: vi.fn((fn) => fn), |
|||
})); |
|||
|
|||
// TODO: getting an error with this test
|
|||
describe('MessageInput Component', () => { |
|||
const mockProps = { |
|||
to: "broadcast" as const, |
|||
channel: 0 as const, |
|||
maxBytes: 100, |
|||
}; |
|||
vi.mock('lucide-react', () => ({ |
|||
SendIcon: vi.fn(() => <svg data-testid="send-icon" />), |
|||
})); |
|||
|
|||
const mockSetMessageDraft = vi.fn(); |
|||
describe('MessageInput', () => { |
|||
const mockSetMessageState = vi.fn(); |
|||
const mockSendText = vi.fn().mockResolvedValue(123); |
|||
const mockSetActiveChat = vi.fn(); |
|||
const mockSetDraft = vi.fn(); |
|||
const mockGetDraft = vi.fn(); |
|||
const mockClearDraft = vi.fn(); |
|||
const mockSendText = vi.fn(); |
|||
|
|||
beforeEach(() => { |
|||
vi.clearAllMocks(); |
|||
|
|||
(useDevice as Mock).mockReturnValue({ |
|||
(useDevice as ReturnType<typeof vi.fn>).mockReturnValue({ |
|||
connection: { |
|||
sendText: mockSendText, |
|||
}, |
|||
setMessageState: mockSetMessageState, |
|||
messageDraft: "", |
|||
setMessageDraft: mockSetMessageDraft, |
|||
hardware: { |
|||
myNodeNum: 1234567890, |
|||
}, |
|||
}); |
|||
}); |
|||
|
|||
it('renders correctly with initial state', () => { |
|||
render(<MessageInput {...mockProps} />); |
|||
|
|||
expect(screen.getByPlaceholderText('Enter Message')).toBeInTheDocument(); |
|||
expect(screen.getByTestId('send-icon')).toBeInTheDocument(); |
|||
|
|||
expect(screen.getByText('0/100')).toBeInTheDocument(); |
|||
}); |
|||
|
|||
it('updates local draft and byte count when typing', () => { |
|||
render(<MessageInput {...mockProps} />); |
|||
|
|||
const inputField = screen.getByPlaceholderText('Enter Message'); |
|||
fireEvent.change(inputField, { target: { value: 'Hello' } }) |
|||
|
|||
expect(screen.getByText('5/100')).toBeInTheDocument(); |
|||
expect(inputField).toHaveValue('Hello'); |
|||
expect(mockSetMessageDraft).toHaveBeenCalledWith('Hello'); |
|||
}); |
|||
|
|||
it.skip('does not allow input exceeding max bytes', () => { |
|||
render(<MessageInput {...mockProps} maxBytes={5} />); |
|||
|
|||
const inputField = screen.getByPlaceholderText('Enter Message'); |
|||
|
|||
expect(screen.getByText('0/100')).toBeInTheDocument(); |
|||
|
|||
userEvent.type(inputField, 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis p') |
|||
(useMessageStore as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ |
|||
setMessageState: mockSetMessageState, |
|||
activeChat: 123, |
|||
setDraft: mockSetDraft, |
|||
getDraft: mockGetDraft, |
|||
clearDraft: mockClearDraft, |
|||
}); |
|||
|
|||
expect(screen.getByText('100/100')).toBeInTheDocument(); |
|||
expect(inputField).toHaveValue('Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean m'); |
|||
mockSetMessageState.mockClear(); |
|||
mockSetActiveChat.mockClear(); |
|||
mockSetDraft.mockClear(); |
|||
mockGetDraft.mockClear(); |
|||
mockClearDraft.mockClear(); |
|||
mockSendText.mockClear(); |
|||
(debounce as ReturnType<typeof vi.fn>).mockImplementation((fn) => fn); |
|||
}); |
|||
|
|||
it.skip('sends message and resets form when submitting', async () => { |
|||
try { |
|||
render(<MessageInput {...mockProps} />); |
|||
|
|||
const inputField = screen.getByPlaceholderText('Enter Message'); |
|||
const submitButton = screen.getByText('Send'); |
|||
|
|||
fireEvent.change(inputField, { target: { value: 'Test Message' } }); |
|||
fireEvent.click(submitButton); |
|||
|
|||
const form = screen.getByRole('form'); |
|||
fireEvent.submit(form); |
|||
|
|||
expect(mockSendText).toHaveBeenCalledWith('Test message', 'broadcast', true, 0); |
|||
|
|||
await waitFor(() => { |
|||
expect(mockSetMessageState).toHaveBeenCalledWith( |
|||
'broadcast', |
|||
0, |
|||
'broadcast', |
|||
1234567890, |
|||
123, |
|||
'ack' |
|||
); |
|||
const renderComponent = (props: { to: Types.Destination; channel: Types.ChannelNumber; maxBytes: number }) => { |
|||
render(<MessageInput {...props} />); |
|||
}; |
|||
|
|||
it.skip('sends text message and updates state to Ack on submit', async () => { |
|||
renderComponent({ to: 2, channel: 3, maxBytes: 256 }); |
|||
const inputElement = screen.getByPlaceholderText('Enter Message') as HTMLInputElement; |
|||
fireEvent.change(inputElement, { target: { value: 'Hello' } }); |
|||
const formElement = screen.getByRole('form'); |
|||
fireEvent.submit(formElement); |
|||
|
|||
await waitFor(() => { |
|||
expect(mockSendText).toHaveBeenCalledWith('Hello', 2, true, 3); |
|||
expect(mockSetMessageState).toHaveBeenCalledWith({ |
|||
type: 'direct', |
|||
key: 123, |
|||
messageId: undefined, |
|||
newState: 'ack', |
|||
}); |
|||
|
|||
expect(inputField).toHaveValue(''); |
|||
expect(screen.getByText('0/100')).toBeInTheDocument(); |
|||
expect(mockSetMessageDraft).toHaveBeenCalledWith(''); |
|||
} catch (e) { |
|||
console.error(e); |
|||
} |
|||
expect(mockClearDraft).toHaveBeenCalledWith(2); |
|||
expect(inputElement.value).toBe(''); |
|||
expect(screen.getByTestId('byte-counter')).toHaveTextContent('0/256'); |
|||
}); |
|||
}); |
|||
it('prevents sending empty messages', () => { |
|||
render(<MessageInput {...mockProps} />); |
|||
|
|||
const form = screen.getByPlaceholderText('Enter Message') |
|||
fireEvent.submit(form); |
|||
|
|||
expect(mockSendText).not.toHaveBeenCalled(); |
|||
it.skip('sends broadcast message if to is "broadcast" and updates state to Ack', async () => { |
|||
renderComponent({ to: 'broadcast', channel: 5, maxBytes: 256 }); |
|||
const inputElement = screen.getByPlaceholderText('Enter Message') as HTMLInputElement; |
|||
fireEvent.change(inputElement, { target: { value: 'Broadcast message' } }); |
|||
const formElement = screen.getByRole('form'); |
|||
fireEvent.submit(formElement); |
|||
|
|||
await waitFor(() => { |
|||
expect(mockSendText).toHaveBeenCalledWith('Broadcast message', 'broadcast', true, 5); |
|||
expect(mockSetMessageState).toHaveBeenCalledWith({ |
|||
type: 'broadcast', |
|||
key: 123, |
|||
messageId: undefined, |
|||
newState: 'ack', |
|||
}); |
|||
expect(mockClearDraft).toHaveBeenCalledWith('broadcast'); |
|||
expect(inputElement.value).toBe(''); |
|||
expect(screen.getByTestId('byte-counter')).toHaveTextContent('0/256'); |
|||
}); |
|||
}); |
|||
|
|||
it('initializes with existing message draft', () => { |
|||
(useDevice as Mock).mockReturnValue({ |
|||
connection: { |
|||
sendText: mockSendText, |
|||
}, |
|||
setMessageState: mockSetMessageState, |
|||
messageDraft: "Existing draft", |
|||
setMessageDraft: mockSetMessageDraft, |
|||
isQueueingMessages: false, |
|||
queueStatus: { free: 10 }, |
|||
hardware: { |
|||
myNodeNum: 1234567890, |
|||
}, |
|||
it('updates state to Failed if sendText throws an error', async () => { |
|||
mockSendText.mockRejectedValue({ id: 456 }); |
|||
renderComponent({ to: 3, channel: 1, maxBytes: 256 }); |
|||
const inputElement = screen.getByPlaceholderText('Enter Message') as HTMLInputElement; |
|||
fireEvent.change(inputElement, { target: { value: 'Error message' } }); |
|||
const formElement = screen.getByRole('form'); |
|||
fireEvent.submit(formElement); |
|||
|
|||
await waitFor(() => { |
|||
expect(mockSendText).toHaveBeenCalledWith('Error message', 3, true, 1); |
|||
expect(mockSetMessageState).toHaveBeenCalledWith({ |
|||
type: 'direct', |
|||
key: 123, |
|||
messageId: 456, |
|||
newState: 'failed', |
|||
}); |
|||
expect(mockClearDraft).toHaveBeenCalledWith(3); |
|||
expect(inputElement.value).toBe(''); |
|||
expect(screen.getByTestId('byte-counter')).toHaveTextContent('0/256'); |
|||
}); |
|||
|
|||
render(<MessageInput {...mockProps} />); |
|||
|
|||
const inputField = screen.getByRole('textbox'); |
|||
|
|||
expect(inputField).toHaveValue('Existing draft'); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue