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 { render, screen, fireEvent, waitFor } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
import { vi, describe, it, expect, beforeEach } from 'vitest'; |
||||
|
import { MessageInput } from './MessageInput.tsx'; |
||||
vi.mock("@core/stores/deviceStore.ts", () => ({ |
import { useDevice } from '@core/stores/deviceStore.ts'; |
||||
useDevice: vi.fn(), |
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", () => ({ |
vi.mock('@components/UI/Input.tsx', () => ({ |
||||
debounce: (fn: () => void) => fn, |
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", () => ({ |
vi.mock('@core/stores/deviceStore.ts', () => ({ |
||||
Button: ({ children, ...props }: { children: React.ReactNode }) => <button {...props}>{children}</button> |
useDevice: vi.fn(), |
||||
})); |
})); |
||||
|
|
||||
vi.mock("@components/UI/Input.tsx", () => ({ |
vi.mock('@core/stores/messageStore.ts', () => ({ |
||||
Input: (props: any) => <input {...props} /> |
useMessageStore: vi.fn(), |
||||
|
MessageState: { |
||||
|
Ack: 'ack', |
||||
|
Waiting: 'waiting', |
||||
|
Failed: 'failed', |
||||
|
}, |
||||
|
MessageType: { |
||||
|
Direct: 'direct', |
||||
|
Broadcast: 'broadcast', |
||||
|
}, |
||||
})); |
})); |
||||
|
|
||||
vi.mock("lucide-react", () => ({ |
vi.mock('@core/utils/debounce.ts', () => ({ |
||||
SendIcon: () => <div data-testid="send-icon">Send</div> |
debounce: vi.fn((fn) => fn), |
||||
})); |
})); |
||||
|
|
||||
// TODO: getting an error with this test
|
vi.mock('lucide-react', () => ({ |
||||
describe('MessageInput Component', () => { |
SendIcon: vi.fn(() => <svg data-testid="send-icon" />), |
||||
const mockProps = { |
})); |
||||
to: "broadcast" as const, |
|
||||
channel: 0 as const, |
|
||||
maxBytes: 100, |
|
||||
}; |
|
||||
|
|
||||
const mockSetMessageDraft = vi.fn(); |
describe('MessageInput', () => { |
||||
const mockSetMessageState = vi.fn(); |
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(() => { |
beforeEach(() => { |
||||
vi.clearAllMocks(); |
(useDevice as ReturnType<typeof vi.fn>).mockReturnValue({ |
||||
|
|
||||
(useDevice as Mock).mockReturnValue({ |
|
||||
connection: { |
connection: { |
||||
sendText: mockSendText, |
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(); |
(useMessageStore as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ |
||||
}); |
setMessageState: mockSetMessageState, |
||||
|
activeChat: 123, |
||||
it('updates local draft and byte count when typing', () => { |
setDraft: mockSetDraft, |
||||
render(<MessageInput {...mockProps} />); |
getDraft: mockGetDraft, |
||||
|
clearDraft: mockClearDraft, |
||||
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') |
|
||||
|
|
||||
expect(screen.getByText('100/100')).toBeInTheDocument(); |
mockSetMessageState.mockClear(); |
||||
expect(inputField).toHaveValue('Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean m'); |
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 () => { |
const renderComponent = (props: { to: Types.Destination; channel: Types.ChannelNumber; maxBytes: number }) => { |
||||
try { |
render(<MessageInput {...props} />); |
||||
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' |
|
||||
); |
|
||||
|
|
||||
|
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(mockClearDraft).toHaveBeenCalledWith(2); |
||||
expect(inputField).toHaveValue(''); |
expect(inputElement.value).toBe(''); |
||||
expect(screen.getByText('0/100')).toBeInTheDocument(); |
expect(screen.getByTestId('byte-counter')).toHaveTextContent('0/256'); |
||||
expect(mockSetMessageDraft).toHaveBeenCalledWith(''); |
}); |
||||
} catch (e) { |
|
||||
console.error(e); |
|
||||
} |
|
||||
}); |
}); |
||||
it('prevents sending empty messages', () => { |
|
||||
render(<MessageInput {...mockProps} />); |
|
||||
|
|
||||
const form = screen.getByPlaceholderText('Enter Message') |
it.skip('sends broadcast message if to is "broadcast" and updates state to Ack', async () => { |
||||
fireEvent.submit(form); |
renderComponent({ to: 'broadcast', channel: 5, maxBytes: 256 }); |
||||
|
const inputElement = screen.getByPlaceholderText('Enter Message') as HTMLInputElement; |
||||
expect(mockSendText).not.toHaveBeenCalled(); |
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', () => { |
it('updates state to Failed if sendText throws an error', async () => { |
||||
(useDevice as Mock).mockReturnValue({ |
mockSendText.mockRejectedValue({ id: 456 }); |
||||
connection: { |
renderComponent({ to: 3, channel: 1, maxBytes: 256 }); |
||||
sendText: mockSendText, |
const inputElement = screen.getByPlaceholderText('Enter Message') as HTMLInputElement; |
||||
}, |
fireEvent.change(inputElement, { target: { value: 'Error message' } }); |
||||
setMessageState: mockSetMessageState, |
const formElement = screen.getByRole('form'); |
||||
messageDraft: "Existing draft", |
fireEvent.submit(formElement); |
||||
setMessageDraft: mockSetMessageDraft, |
|
||||
isQueueingMessages: false, |
await waitFor(() => { |
||||
queueStatus: { free: 10 }, |
expect(mockSendText).toHaveBeenCalledWith('Error message', 3, true, 1); |
||||
hardware: { |
expect(mockSetMessageState).toHaveBeenCalledWith({ |
||||
myNodeNum: 1234567890, |
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