// @vitest-environment jsdom import { cleanup, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { InstanceContext } from '../app-shell.js'; import { MessagesModule, type MessagesDataSource } from './messages-module.js'; const instance: InstanceContext = { id: 'alpha', name: 'Alpha', origin: 'http://192.168.1.2', status: 'unknown', authentication: 'unknown', freshness: 'unknown', }; afterEach(cleanup); describe('MessagesModule', () => { it('organizes messages as conversations and starts a new conversation before composing', async () => { const user = userEvent.setup(); const dataSource: MessagesDataSource = { load: vi.fn().mockResolvedValue({ messages: [ { id: '32', direction: 'incoming', phoneNumber: '10086', content: '余额提醒', timestamp: '2026-07-18 09:09:20', status: 'received', }, ], }), send: vi.fn(), }; render(); const conversations = await screen.findByRole('region', { name: '短信对话' }); expect(within(conversations).getByRole('button', { name: /10086/ })).toBeTruthy(); expect(screen.getByRole('region', { name: '当前短信会话' }).textContent).toContain( '选择一个对话', ); await user.click(within(conversations).getByRole('button', { name: '新建对话' })); const dialog = screen.getByRole('dialog', { name: '新建对话' }); expect(within(dialog).getByRole('textbox', { name: '手机号' })).toBeTruthy(); expect(screen.queryByRole('textbox', { name: '短信内容' })).toBeNull(); }); it('shows the real message list including content and never exposes PDU', async () => { const user = userEvent.setup(); const dataSource: MessagesDataSource = { load: vi.fn().mockResolvedValue({ messages: [ { id: '32', direction: 'incoming', phoneNumber: '10086', content: '余额提醒', timestamp: '2026-07-18 09:09:20', status: 'received', pdu: 'secret-pdu', }, ], }), send: vi.fn(), }; render(); await user.click(await screen.findByRole('button', { name: /10086/ })); expect((await screen.findAllByText('余额提醒')).length).toBeGreaterThanOrEqual(2); expect(screen.getByText(/收到 · 10086/)).toBeTruthy(); expect(document.body.textContent).not.toContain('secret-pdu'); }); it('clears recipient-bound drafts when the conversation changes', async () => { const user = userEvent.setup(); const dataSource: MessagesDataSource = { load: vi.fn().mockResolvedValue({ messages: [ { id: 'one', direction: 'incoming', phoneNumber: '10086', content: 'A', timestamp: '2026-07-18 09:09:20', status: 'received', }, { id: 'two', direction: 'incoming', phoneNumber: '10010', content: 'B', timestamp: '2026-07-18 09:10:20', status: 'received', }, ], }), send: vi.fn(), }; render(); await user.click(await screen.findByRole('button', { name: /10086/ })); await user.type(screen.getByRole('textbox', { name: '短信内容' }), 'draft for 10086'); await user.click(screen.getByRole('button', { name: /10010/ })); expect((screen.getByRole('textbox', { name: '短信内容' }) as HTMLTextAreaElement).value).toBe( '', ); }); it('discards a pending confirmation when the instance owner changes', async () => { const user = userEvent.setup(); const send = vi.fn().mockResolvedValue(undefined); const dataSource: MessagesDataSource = { load: vi.fn().mockResolvedValue({ messages: [] }), send, }; const second = { ...instance, id: 'beta', name: 'Beta' }; const { rerender } = render(); await user.click(await screen.findByRole('button', { name: '新建对话' })); const dialog = screen.getByRole('dialog', { name: '新建对话' }); await user.type(within(dialog).getByRole('textbox', { name: '手机号' }), '10086'); await user.click(within(dialog).getByRole('button', { name: '开始对话' })); await user.type(screen.getByRole('textbox', { name: '短信内容' }), 'owner-bound draft'); await user.click(screen.getByRole('button', { name: '发送短信' })); expect(screen.getByRole('region', { name: '确认发送短信' })).toBeTruthy(); rerender(); expect(screen.queryByRole('region', { name: '确认发送短信' })).toBeNull(); expect(send).not.toHaveBeenCalled(); }); it('drops an old confirmation when starting another recipient conversation', async () => { const user = userEvent.setup(); const dataSource: MessagesDataSource = { load: vi.fn().mockResolvedValue({ messages: [ { id: 'one', direction: 'incoming', phoneNumber: '10086', content: 'A', timestamp: '2026-07-18 09:09:20', status: 'received', }, ], }), send: vi.fn(), }; render(); await user.click(await screen.findByRole('button', { name: /10086/ })); await user.type(screen.getByRole('textbox', { name: '短信内容' }), 'old confirmation'); await user.click(screen.getByRole('button', { name: '发送短信' })); expect(screen.getByRole('region', { name: '确认发送短信' })).toBeTruthy(); await user.click(screen.getByRole('button', { name: '新建对话' })); const dialog = screen.getByRole('dialog', { name: '新建对话' }); await user.type(within(dialog).getByRole('textbox', { name: '手机号' }), '10010'); await user.click(within(dialog).getByRole('button', { name: '开始对话' })); expect(screen.queryByRole('region', { name: '确认发送短信' })).toBeNull(); }); it('preserves a new recipient draft when an earlier send completes late', async () => { const user = userEvent.setup(); let resolveSend: (() => void) | undefined; const send = vi.fn( () => new Promise((resolve) => { resolveSend = resolve; }), ); const dataSource: MessagesDataSource = { load: vi.fn().mockResolvedValue({ messages: [ { id: 'one', direction: 'incoming', phoneNumber: '10086', content: 'A', timestamp: '2026-07-18 09:09:20', status: 'received', }, { id: 'two', direction: 'incoming', phoneNumber: '10010', content: 'B', timestamp: '2026-07-18 09:10:20', status: 'received', }, ], }), send, }; render(); await user.click(await screen.findByRole('button', { name: /10086/ })); await user.type(screen.getByRole('textbox', { name: '短信内容' }), 'send to 10086'); await user.click(screen.getByRole('button', { name: '发送短信' })); await user.click(screen.getByRole('button', { name: '确认并发送' })); await user.click(screen.getByRole('button', { name: /10010/ })); await user.type(screen.getByRole('textbox', { name: '短信内容' }), 'draft for 10010'); resolveSend?.(); expect(await screen.findByDisplayValue('draft for 10010')).toBeTruthy(); }); it('requires an explicit review step before it submits exactly once', async () => { const user = userEvent.setup(); const send = vi.fn().mockResolvedValue(undefined); const dataSource: MessagesDataSource = { load: vi.fn().mockResolvedValue({ messages: [] }), send, }; render(); await user.click(await screen.findByRole('button', { name: '新建对话' })); const dialog = screen.getByRole('dialog', { name: '新建对话' }); await user.type(within(dialog).getByRole('textbox', { name: '手机号' }), '10086'); await user.click(within(dialog).getByRole('button', { name: '开始对话' })); await user.type(screen.getByRole('textbox', { name: '短信内容' }), 'CXLL'); await user.click(screen.getByRole('button', { name: '发送短信' })); expect(send).not.toHaveBeenCalled(); const confirmation = screen.getByRole('region', { name: '确认发送短信' }); expect(confirmation.textContent).toContain('10086'); expect(confirmation.textContent).toContain('CXLL'); await user.click(within(confirmation).getByRole('button', { name: '确认并发送' })); expect(send).toHaveBeenCalledTimes(1); expect(send).toHaveBeenCalledWith('alpha', { phoneNumber: '10086', content: 'CXLL' }); expect((await screen.findByRole('status')).textContent).toContain('短信已提交发送'); }); });