// @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('短信已提交发送');
});
});