You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
862 lines
30 KiB
862 lines
30 KiB
/**
|
|
* 问答页对话管理与流式输出自动化测试脚本
|
|
* 测试流程:登录 → 进入问答Tab → 创建对话 → 发送消息 → 验证流式输出 → 对话管理
|
|
*/
|
|
const { chromium } = require('playwright');
|
|
|
|
const APP_URL = 'http://localhost:8081';
|
|
const TEST_PHONE = '13800138000';
|
|
const TEST_CODE = '123456';
|
|
|
|
// 测试结果统计
|
|
const testResults = {
|
|
passed: 0,
|
|
failed: 0,
|
|
tests: []
|
|
};
|
|
|
|
function logTest(name, passed, detail = '') {
|
|
const status = passed ? '✓' : '✗';
|
|
const msg = `${status} ${name}${detail ? ': ' + detail : ''}`;
|
|
console.log(msg);
|
|
testResults.tests.push({ name, passed, detail });
|
|
if (passed) testResults.passed++;
|
|
else testResults.failed++;
|
|
}
|
|
|
|
async function login(page) {
|
|
console.log('\n【步骤1】检查登录状态...');
|
|
|
|
const loginBtn = page.locator('text=登录').first();
|
|
if (!(await loginBtn.isVisible({ timeout: 2000 }).catch(() => false))) {
|
|
logTest('已登录状态', true, '跳过登录流程');
|
|
return true;
|
|
}
|
|
|
|
console.log(' 执行登录流程...');
|
|
|
|
// 输入手机号
|
|
const phoneInput = page.locator('input').first();
|
|
await phoneInput.fill(TEST_PHONE);
|
|
await page.waitForTimeout(300);
|
|
|
|
// 获取验证码
|
|
const getCodeBtn = page.locator('text=获取验证码').first();
|
|
if (await getCodeBtn.isVisible()) {
|
|
await getCodeBtn.click();
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
// 输入验证码
|
|
const codeInput = page.locator('input').nth(1);
|
|
await codeInput.fill(TEST_CODE);
|
|
await page.waitForTimeout(300);
|
|
|
|
// 点击登录
|
|
await loginBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
|
|
logTest('登录', true);
|
|
return true;
|
|
}
|
|
|
|
async function navigateToChatTab(page) {
|
|
console.log('\n【步骤2】导航到问答Tab...');
|
|
|
|
// 点击问答Tab
|
|
const chatTabPos = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
if (el.textContent?.trim() === '问答' && el.children.length === 0) {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width > 0 && rect.height > 0) {
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (chatTabPos) {
|
|
await page.mouse.click(chatTabPos.x, chatTabPos.y);
|
|
await page.waitForTimeout(2000);
|
|
logTest('导航到问答页面', true);
|
|
return true;
|
|
}
|
|
|
|
logTest('导航到问答页面', false, '未找到问答Tab');
|
|
return false;
|
|
}
|
|
|
|
// 确保进入到具体对话页面(ChatDetailScreen)
|
|
async function ensureInChatDetail(page) {
|
|
console.log('\n【步骤2.1】检查是否在对话详情页...');
|
|
|
|
// 检查是否已在 ChatDetailScreen(有"对话管理"按钮)
|
|
const inDetail = await page.evaluate(() => {
|
|
return document.body.innerText.includes('对话管理') && document.body.innerText.includes('AI健康助手');
|
|
});
|
|
|
|
if (inDetail) {
|
|
logTest('已在对话详情页', true);
|
|
return true;
|
|
}
|
|
|
|
// 如果在 ChatListScreen,需要创建新对话
|
|
console.log(' 当前在对话列表页,尝试创建新对话...');
|
|
|
|
// 优先点击"开始对话"按钮(空状态时显示)
|
|
let created = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
const text = el.textContent?.trim();
|
|
if (text === '开始对话' && el.children.length === 0) {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width > 0 && rect.height > 0) {
|
|
el.click();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!created) {
|
|
// 备用:点击 FAB "新建对话"按钮
|
|
const fabBtn = page.locator('text=新建对话').first();
|
|
if (await fabBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await fabBtn.click();
|
|
created = true;
|
|
}
|
|
}
|
|
|
|
if (created) {
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 再次检查是否进入了详情页
|
|
const nowInDetail = await page.evaluate(() => {
|
|
return document.body.innerText.includes('对话管理') || document.body.innerText.includes('AI健康助手');
|
|
});
|
|
|
|
logTest('创建新对话进入详情页', nowInDetail);
|
|
return nowInDetail;
|
|
}
|
|
|
|
logTest('进入对话详情页', false, '无法创建对话');
|
|
return false;
|
|
}
|
|
|
|
async function testConversationManagement(page) {
|
|
console.log('\n【步骤3】测试对话管理功能...');
|
|
|
|
// 截图当前状态
|
|
await page.screenshot({ path: 'tests/screenshots/chat-page.png' });
|
|
|
|
// 检查页面类型
|
|
const pageType = await page.evaluate(() => {
|
|
const text = document.body.innerText;
|
|
if (text.includes('对话管理')) return 'detail';
|
|
if (text.includes('历史记录')) return 'list';
|
|
return 'unknown';
|
|
});
|
|
|
|
console.log(` 当前页面类型: ${pageType}`);
|
|
|
|
// 根据页面类型处理
|
|
if (pageType === 'detail') {
|
|
// 在详情页,点击"对话管理"按钮
|
|
const manageBtnPos = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
if (el.textContent?.trim() === '对话管理' && el.children.length === 0) {
|
|
const rect = el.getBoundingClientRect();
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (!manageBtnPos) {
|
|
logTest('对话管理按钮显示', false, '未找到对话管理按钮');
|
|
return false;
|
|
}
|
|
|
|
logTest('对话管理按钮显示', true);
|
|
|
|
await page.mouse.click(manageBtnPos.x, manageBtnPos.y);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 验证对话管理弹窗打开(详情页弹窗有"+ 新建对话")
|
|
const modalOpened = await page.evaluate(() => {
|
|
return document.body.innerText.includes('+ 新建对话');
|
|
});
|
|
|
|
logTest('对话管理弹窗打开', modalOpened);
|
|
|
|
if (!modalOpened) return true; // 没有弹窗也继续
|
|
|
|
await page.screenshot({ path: 'tests/screenshots/chat-management-modal.png' });
|
|
|
|
// 关闭弹窗 - 点击关闭按钮(X)或背景
|
|
const closed = await page.evaluate(() => {
|
|
// 尝试点击关闭按钮
|
|
const closeBtn = document.querySelector('[aria-label="Close modal"]') ||
|
|
document.querySelector('button[icon="close"]');
|
|
if (closeBtn) {
|
|
closeBtn.click();
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!closed) {
|
|
// 备用:按 Escape
|
|
await page.keyboard.press('Escape');
|
|
}
|
|
await page.waitForTimeout(800);
|
|
|
|
// 再次检查弹窗是否关闭
|
|
const stillOpen = await page.evaluate(() => {
|
|
return document.body.innerText.includes('+ 新建对话');
|
|
});
|
|
|
|
if (stillOpen) {
|
|
// 再试一次:点击弹窗外部
|
|
await page.mouse.click(50, 50);
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
return true;
|
|
|
|
} else if (pageType === 'list') {
|
|
// 在列表页,先点击"开始对话"按钮创建新对话(最直接的方式)
|
|
const startChatBtn = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
if (el.textContent?.trim() === '开始对话' && el.children.length === 0) {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width > 0 && rect.height > 0 && rect.width < 200) {
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (startChatBtn) {
|
|
logTest('开始对话按钮显示', true);
|
|
await page.mouse.click(startChatBtn.x, startChatBtn.y);
|
|
console.log(' 点击开始对话按钮,等待页面跳转...');
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 验证是否已进入详情页
|
|
const inDetailNow = await page.evaluate(() => {
|
|
const text = document.body.innerText;
|
|
return text.includes('AI健康助手') || text.includes('对话管理');
|
|
});
|
|
|
|
if (inDetailNow) {
|
|
logTest('创建新对话', true);
|
|
logTest('对话管理按钮显示', true, '进入详情页');
|
|
await page.screenshot({ path: 'tests/screenshots/chat-detail-entered.png' });
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 备用方案:点击右下角 FAB 按钮
|
|
console.log(' 尝试点击 FAB 新建对话按钮...');
|
|
const fabBtn = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
const text = el.textContent?.trim();
|
|
// FAB 按钮显示"+ 新建对话"或类似
|
|
if ((text === '新建对话' || text === '+ 新建对话') && el.children.length <= 2) {
|
|
const rect = el.getBoundingClientRect();
|
|
// FAB 通常在右下角
|
|
if (rect.width > 0 && rect.height > 0 && rect.y > 400) {
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (fabBtn) {
|
|
await page.mouse.click(fabBtn.x, fabBtn.y);
|
|
console.log(' 点击 FAB 按钮,等待页面跳转...');
|
|
await page.waitForTimeout(3000);
|
|
|
|
const inDetailNow = await page.evaluate(() => {
|
|
const text = document.body.innerText;
|
|
return text.includes('AI健康助手') || text.includes('对话管理');
|
|
});
|
|
|
|
if (inDetailNow) {
|
|
logTest('开始对话按钮显示', false, 'FAB 方式');
|
|
logTest('创建新对话', true);
|
|
logTest('对话管理按钮显示', true, '进入详情页');
|
|
await page.screenshot({ path: 'tests/screenshots/chat-detail-entered.png' });
|
|
return true;
|
|
}
|
|
}
|
|
|
|
logTest('创建新对话', false, '两种方式都失败');
|
|
await page.screenshot({ path: 'tests/screenshots/chat-create-failed.png' });
|
|
return false;
|
|
}
|
|
|
|
logTest('对话管理按钮显示', false, '页面状态异常');
|
|
return false;
|
|
}
|
|
|
|
async function testSendMessage(page) {
|
|
console.log('\n【步骤4】测试发送消息...');
|
|
|
|
// 等待页面完全加载
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 截图当前页面状态
|
|
await page.screenshot({ path: 'tests/screenshots/chat-before-input.png' });
|
|
|
|
// 检查页面状态
|
|
const pageState = await page.evaluate(() => {
|
|
const text = document.body.innerText;
|
|
const inputs = document.querySelectorAll('input, textarea');
|
|
let inputInfo = [];
|
|
inputs.forEach(inp => {
|
|
const rect = inp.getBoundingClientRect();
|
|
inputInfo.push({
|
|
tag: inp.tagName,
|
|
placeholder: inp.placeholder,
|
|
visible: rect.width > 0 && rect.height > 0,
|
|
pos: { x: rect.x, y: rect.y, w: rect.width, h: rect.height }
|
|
});
|
|
});
|
|
return {
|
|
hasAIAssistant: text.includes('AI健康助手'),
|
|
hasDialogMgr: text.includes('对话管理'),
|
|
hasInputPlaceholder: text.includes('请输入您的健康问题'),
|
|
inputCount: inputs.length,
|
|
inputs: inputInfo,
|
|
bodySnippet: text.substring(0, 300)
|
|
};
|
|
});
|
|
|
|
console.log(` 页面状态: AI健康助手=${pageState.hasAIAssistant}, 对话管理=${pageState.hasDialogMgr}`);
|
|
console.log(` 输入框数量: ${pageState.inputCount}`);
|
|
if (pageState.inputs.length > 0) {
|
|
console.log(` 输入框信息:`, JSON.stringify(pageState.inputs));
|
|
}
|
|
|
|
// 查找输入框
|
|
const inputVisible = pageState.hasInputPlaceholder || pageState.inputs.some(i =>
|
|
i.placeholder?.includes('健康') || i.placeholder?.includes('问题') || i.visible
|
|
);
|
|
|
|
logTest('消息输入框显示', inputVisible);
|
|
|
|
const testMessage = '你好,请问感冒了应该怎么办?';
|
|
|
|
// 使用 Playwright 的 type 方法,模拟真实键盘输入
|
|
// 这样可以正确触发 React Native 的 onChangeText
|
|
const input = page.locator('input[placeholder*="健康"], input[placeholder*="问题"], textarea').first();
|
|
|
|
if (await input.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
// 先清空,再逐字输入
|
|
await input.click();
|
|
await page.waitForTimeout(300);
|
|
await input.fill(''); // 清空
|
|
await page.waitForTimeout(200);
|
|
|
|
// 使用 type 模拟键盘输入,这样能触发 React Native 状态更新
|
|
await input.type(testMessage, { delay: 30 });
|
|
console.log(' 已输入消息内容');
|
|
logTest('输入消息内容', true);
|
|
} else {
|
|
// 备用:直接操作 DOM
|
|
const filled = await page.evaluate((msg) => {
|
|
const inputs = document.querySelectorAll('input, textarea');
|
|
for (const input of inputs) {
|
|
if (input.offsetParent !== null) {
|
|
// 模拟 React Native 的输入
|
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
window.HTMLInputElement.prototype, 'value'
|
|
)?.set;
|
|
if (nativeInputValueSetter) {
|
|
nativeInputValueSetter.call(input, msg);
|
|
} else {
|
|
input.value = msg;
|
|
}
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}, testMessage);
|
|
logTest('输入消息内容', filled, filled ? '使用 DOM 操作' : '失败');
|
|
}
|
|
|
|
await page.waitForTimeout(800);
|
|
await page.screenshot({ path: 'tests/screenshots/chat-input-filled.png' });
|
|
|
|
// 等待发送按钮出现(只有输入内容后才显示)
|
|
console.log(' 等待发送按钮出现...');
|
|
await page.waitForTimeout(500);
|
|
|
|
// 点击发送按钮 - 查找蓝色的"发送"文字按钮
|
|
let sendSuccess = false;
|
|
for (let retry = 0; retry < 3; retry++) {
|
|
const sendBtnPos = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
const text = el.textContent?.trim();
|
|
if (text === '发送' && el.children.length === 0) {
|
|
const rect = el.getBoundingClientRect();
|
|
// 发送按钮通常在右侧底部
|
|
if (rect.width > 0 && rect.height > 0 && rect.y > window.innerHeight * 0.7) {
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (sendBtnPos) {
|
|
console.log(` 找到发送按钮 at (${sendBtnPos.x.toFixed(0)}, ${sendBtnPos.y.toFixed(0)})`);
|
|
await page.mouse.click(sendBtnPos.x, sendBtnPos.y);
|
|
sendSuccess = true;
|
|
break;
|
|
}
|
|
|
|
console.log(` 未找到发送按钮,重试 ${retry + 1}/3...`);
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
if (!sendSuccess) {
|
|
// 备用方案:使用 Playwright locator
|
|
const sendBtn = page.locator('text=发送').last();
|
|
if (await sendBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await sendBtn.click({ force: true });
|
|
sendSuccess = true;
|
|
console.log(' 使用 locator 点击发送');
|
|
}
|
|
}
|
|
|
|
logTest('点击发送按钮', sendSuccess, sendSuccess ? '' : '未找到发送按钮');
|
|
|
|
return true;
|
|
}
|
|
|
|
async function testStreamingOutput(page) {
|
|
console.log('\n【步骤5】测试流式输出与思考过程...');
|
|
|
|
// 等待 AI 开始响应
|
|
console.log(' 等待 AI 响应...');
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 截图发送后状态
|
|
await page.screenshot({ path: 'tests/screenshots/chat-after-send.png' });
|
|
|
|
// 检测思考过程和流式输出
|
|
let previousLength = 0;
|
|
let contentGrowing = false;
|
|
let attempts = 0;
|
|
const maxAttempts = 30; // 增加等待次数,思考过程需要更长时间
|
|
let foundThinking = false;
|
|
let foundAIResponse = false;
|
|
let thinkingContent = '';
|
|
|
|
while (attempts < maxAttempts) {
|
|
attempts++;
|
|
|
|
const pageInfo = await page.evaluate(() => {
|
|
const body = document.body.innerText;
|
|
|
|
// 检查是否有思考过程显示
|
|
const hasThinking = body.includes('思考中') ||
|
|
body.includes('思考过程') ||
|
|
body.includes('💭');
|
|
|
|
// 获取思考内容
|
|
const thinkingElements = document.querySelectorAll('[class*="thinking"]');
|
|
let thinkingText = '';
|
|
thinkingElements.forEach(el => {
|
|
thinkingText += el.textContent || '';
|
|
});
|
|
|
|
// 检查是否有 AI 回复的典型格式
|
|
const hasAIFormat = body.includes('【情况分析】') ||
|
|
body.includes('【建议】') ||
|
|
body.includes('【提醒】') ||
|
|
body.includes('【用药参考】');
|
|
|
|
// 检查用户消息是否已发送
|
|
const inputValue = document.querySelector('input')?.value || '';
|
|
const userMsgSent = !inputValue.includes('感冒');
|
|
|
|
return {
|
|
bodyLength: body.length,
|
|
hasThinking,
|
|
thinkingText,
|
|
hasAIFormat,
|
|
userMsgSent,
|
|
preview: body.substring(0, 500)
|
|
};
|
|
});
|
|
|
|
// 检测内容增长(流式效果)
|
|
if (pageInfo.bodyLength > previousLength && previousLength > 0) {
|
|
contentGrowing = true;
|
|
if (attempts % 5 === 0) {
|
|
console.log(` 内容增长: ${previousLength} → ${pageInfo.bodyLength} 字符`);
|
|
}
|
|
}
|
|
previousLength = pageInfo.bodyLength;
|
|
|
|
// 检测思考过程
|
|
if (pageInfo.hasThinking && !foundThinking) {
|
|
foundThinking = true;
|
|
console.log(' ✓ 检测到思考过程显示');
|
|
// 截图思考过程
|
|
await page.screenshot({ path: 'tests/screenshots/chat-thinking.png' });
|
|
}
|
|
|
|
// 更新思考内容
|
|
if (pageInfo.thinkingText && pageInfo.thinkingText.length > thinkingContent.length) {
|
|
thinkingContent = pageInfo.thinkingText;
|
|
}
|
|
|
|
// 检测最终回复
|
|
if (pageInfo.hasAIFormat) {
|
|
foundAIResponse = true;
|
|
console.log(' ✓ 检测到 AI 正式回复');
|
|
break;
|
|
}
|
|
|
|
// 检查用户消息是否已发送
|
|
if (attempts === 5 && !pageInfo.userMsgSent) {
|
|
console.log(' 用户消息可能未发送,尝试再次点击发送...');
|
|
const sendBtn = page.locator('text=发送').last();
|
|
if (await sendBtn.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
await sendBtn.click({ force: true });
|
|
}
|
|
}
|
|
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
// 最终检查
|
|
if (!foundAIResponse) {
|
|
const finalCheck = await page.evaluate(() => {
|
|
const text = document.body.innerText;
|
|
return {
|
|
hasResponse: text.includes('【') || text.includes('建议') || text.includes('分析'),
|
|
hasThinking: text.includes('思考') || text.includes('💭'),
|
|
text: text.substring(0, 800)
|
|
};
|
|
});
|
|
foundAIResponse = finalCheck.hasResponse;
|
|
if (!foundThinking) foundThinking = finalCheck.hasThinking;
|
|
}
|
|
|
|
// 截图最终状态
|
|
await page.screenshot({ path: 'tests/screenshots/chat-ai-response.png' });
|
|
|
|
// 记录测试结果
|
|
logTest('思考过程显示', foundThinking, foundThinking ? '💭 思考中...' : '未检测到思考过程');
|
|
|
|
if (thinkingContent.length > 20) {
|
|
logTest('思考内容输出', true, `${thinkingContent.length} 字符`);
|
|
} else {
|
|
logTest('思考内容输出', foundThinking, foundThinking ? '有思考显示' : '无思考内容');
|
|
}
|
|
|
|
logTest('AI 正式回复', foundAIResponse, foundAIResponse ? '检测到有效回复' : '未检测到回复');
|
|
|
|
if (contentGrowing) {
|
|
logTest('流式输出效果', true, '内容逐步显示');
|
|
} else {
|
|
logTest('流式输出效果', foundAIResponse, foundAIResponse ? '有响应' : '未检测到流式效果');
|
|
}
|
|
|
|
return foundAIResponse || foundThinking;
|
|
}
|
|
|
|
async function testDeleteConversation(page) {
|
|
console.log('\n【步骤6】测试删除对话功能...');
|
|
|
|
// 打开对话管理弹窗
|
|
const manageBtnPos = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
if (el.textContent?.trim() === '对话管理' && el.children.length === 0) {
|
|
const rect = el.getBoundingClientRect();
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (!manageBtnPos) {
|
|
logTest('打开对话管理', false);
|
|
return false;
|
|
}
|
|
|
|
await page.mouse.click(manageBtnPos.x, manageBtnPos.y);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 获取删除前的对话数量
|
|
const beforeCount = await page.evaluate(() => {
|
|
const items = document.querySelectorAll('[data-testid="conversation-item"]');
|
|
if (items.length > 0) return items.length;
|
|
// 如果没有 testid,查找对话列表项
|
|
const allElements = document.querySelectorAll('*');
|
|
let count = 0;
|
|
for (const el of allElements) {
|
|
const text = el.textContent?.trim();
|
|
if (text === '🗑') {
|
|
count++;
|
|
}
|
|
}
|
|
return count; // 每个对话项有一个删除按钮
|
|
});
|
|
console.log(` 删除前对话数量: ${beforeCount}`);
|
|
|
|
// 查找并点击第一个删除按钮(垃圾桶 emoji 🗑)
|
|
const deleteBtnPos = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
const text = el.textContent?.trim();
|
|
if (text === '🗑') {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width > 0 && rect.height > 0) {
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
logTest('删除按钮显示', !!deleteBtnPos);
|
|
|
|
if (!deleteBtnPos) {
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(500);
|
|
return false;
|
|
}
|
|
|
|
// 点击删除按钮
|
|
console.log(' 点击删除按钮...');
|
|
await page.mouse.click(deleteBtnPos.x, deleteBtnPos.y);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 查找并点击确认删除按钮
|
|
const confirmBtnPos = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
for (const el of allElements) {
|
|
const text = el.textContent?.trim();
|
|
// 查找确认弹窗中的"删除"按钮
|
|
if (text === '删除' && el.tagName !== 'SPAN') {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width > 0 && rect.height > 0 && rect.width < 200) {
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
logTest('确认删除弹窗', !!confirmBtnPos);
|
|
|
|
if (confirmBtnPos) {
|
|
console.log(' 确认删除...');
|
|
await page.mouse.click(confirmBtnPos.x, confirmBtnPos.y);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 验证删除成功(检查是否有成功提示或对话数量减少)
|
|
const afterCount = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
let count = 0;
|
|
for (const el of allElements) {
|
|
const text = el.textContent?.trim();
|
|
if (text === '🗑') {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
});
|
|
console.log(` 删除后对话数量: ${afterCount}`);
|
|
|
|
const deleteSuccess = afterCount < beforeCount || afterCount === 0;
|
|
logTest('删除对话成功', deleteSuccess, deleteSuccess ? `${beforeCount} → ${afterCount}` : '数量未变化');
|
|
}
|
|
|
|
// 关闭弹窗
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(500);
|
|
|
|
return true;
|
|
}
|
|
|
|
async function testHistoryPersistence(page) {
|
|
console.log('\n【步骤7】测试数据持久化(刷新后状态保持)...');
|
|
|
|
// 刷新页面
|
|
await page.reload();
|
|
await page.waitForTimeout(3000);
|
|
|
|
// 刷新后需要重新登录(token 可能在 localStorage,但会过期或需要重新验证)
|
|
await login(page);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 重新导航到问答页
|
|
await navigateToChatTab(page);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 检查页面状态 - 根据之前测试是否删除了对话来判断
|
|
const pageState = await page.evaluate(() => {
|
|
const text = document.body.innerText;
|
|
// 检查是否在详情页(有对话内容)
|
|
const inDetail = text.includes('AI健康助手');
|
|
// 检查是否在列表页
|
|
const inList = text.includes('AI问答') && !text.includes('AI健康助手');
|
|
// 检查是否有对话内容
|
|
const hasContent = text.includes('感冒') || text.includes('你好') || text.includes('【');
|
|
// 检查是否显示空状态
|
|
const isEmpty = text.includes('暂无对话') || text.includes('开始对话');
|
|
|
|
return {
|
|
inDetail,
|
|
inList,
|
|
hasContent,
|
|
isEmpty,
|
|
snippet: text.substring(0, 300)
|
|
};
|
|
});
|
|
|
|
console.log(` 页面状态: 在${pageState.inDetail ? '详情页' : (pageState.inList ? '列表页' : '未知')}`);
|
|
|
|
// 判断持久化是否成功
|
|
// 如果之前删除了所有对话,空状态应该正确显示(这也是正确的持久化)
|
|
// 如果还有对话,应该能看到对话内容
|
|
const persistenceCorrect = pageState.inDetail || pageState.inList || pageState.isEmpty;
|
|
const stateDesc = pageState.hasContent ? '对话内容已保留' :
|
|
pageState.isEmpty ? '空状态正确显示' : '页面状态正确';
|
|
|
|
logTest('数据持久化', persistenceCorrect, stateDesc);
|
|
|
|
await page.screenshot({ path: 'tests/screenshots/chat-after-refresh.png' });
|
|
|
|
return persistenceCorrect;
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(' 问答页对话管理与流式输出自动化测试');
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log('\n测试范围:');
|
|
console.log(' - 对话管理:打开弹窗、新建对话、删除对话');
|
|
console.log(' - 发送消息:输入内容、点击发送');
|
|
console.log(' - 流式输出:AI 响应逐字显示');
|
|
console.log(' - 历史持久化:刷新后对话保留');
|
|
console.log('');
|
|
|
|
const browser = await chromium.launch({ headless: false });
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1280, height: 800 }
|
|
});
|
|
const page = await context.newPage();
|
|
|
|
// 监听控制台
|
|
page.on('console', msg => {
|
|
const text = msg.text();
|
|
// 捕获与消息发送和思考过程相关的所有日志
|
|
if (text.includes('[SendMessage]') || text.includes('[Chat]') ||
|
|
text.includes('SSE') || text.includes('stream') ||
|
|
text.includes('thinking') || text.includes('思考')) {
|
|
console.log(` [Browser ${msg.type()}] ${text.substring(0, 250)}`);
|
|
}
|
|
// 仅输出真正的错误(排除 constitution/result 的 400 - 这是正常情况)
|
|
if (msg.type() === 'error' && !text.includes('constitution')) {
|
|
console.log(` [Browser error] ${text.substring(0, 200)}`);
|
|
}
|
|
});
|
|
|
|
try {
|
|
console.log('\n打开应用...');
|
|
await page.goto(APP_URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// 执行测试步骤
|
|
const loginOk = await login(page);
|
|
if (!loginOk) throw new Error('登录失败');
|
|
|
|
const navOk = await navigateToChatTab(page);
|
|
if (!navOk) throw new Error('导航失败');
|
|
|
|
await testConversationManagement(page);
|
|
await testSendMessage(page);
|
|
await testStreamingOutput(page);
|
|
await testDeleteConversation(page);
|
|
await testHistoryPersistence(page);
|
|
|
|
} catch (error) {
|
|
console.error('\n测试中断:', error.message);
|
|
await page.screenshot({ path: 'tests/screenshots/chat-error.png' });
|
|
} finally {
|
|
// 打印测试摘要
|
|
console.log('\n═══════════════════════════════════════════════════════════');
|
|
console.log(' 测试结果摘要');
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(`通过: ${testResults.passed} 失败: ${testResults.failed}`);
|
|
console.log('───────────────────────────────────────────────────────────');
|
|
|
|
// 按类别分组显示
|
|
const categories = {
|
|
'登录与导航': [],
|
|
'对话管理': [],
|
|
'消息发送': [],
|
|
'思考过程': [],
|
|
'流式输出': [],
|
|
'历史持久化': []
|
|
};
|
|
|
|
for (const test of testResults.tests) {
|
|
const icon = test.passed ? '✓' : '✗';
|
|
const line = `${icon} ${test.name}${test.detail ? ' - ' + test.detail : ''}`;
|
|
|
|
if (test.name.includes('登录') || test.name.includes('导航')) {
|
|
categories['登录与导航'].push(line);
|
|
} else if (test.name.includes('对话管理') || test.name.includes('新建') || test.name.includes('删除')) {
|
|
categories['对话管理'].push(line);
|
|
} else if (test.name.includes('输入') || test.name.includes('发送')) {
|
|
categories['消息发送'].push(line);
|
|
} else if (test.name.includes('思考')) {
|
|
categories['思考过程'].push(line);
|
|
} else if (test.name.includes('流式') || test.name.includes('AI')) {
|
|
categories['流式输出'].push(line);
|
|
} else if (test.name.includes('持久化') || test.name.includes('历史') || test.name.includes('数据')) {
|
|
categories['历史持久化'].push(line);
|
|
} else {
|
|
categories['登录与导航'].push(line);
|
|
}
|
|
}
|
|
|
|
for (const [category, tests] of Object.entries(categories)) {
|
|
if (tests.length > 0) {
|
|
console.log(`\n【${category}】`);
|
|
for (const test of tests) {
|
|
console.log(` ${test}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('\n═══════════════════════════════════════════════════════════\n');
|
|
|
|
await browser.close();
|
|
process.exit(testResults.failed > 0 ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
runTests();
|
|
|