Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
532 lines
15 KiB
TypeScript
532 lines
15 KiB
TypeScript
/**
|
||
* WebSocket 实时通信功能端到端测试
|
||
* 测试实时消息推送、状态同步、连接管理等功能
|
||
*/
|
||
|
||
import { test, expect, Page } from '@playwright/test';
|
||
|
||
const TEST_CONFIG = {
|
||
baseURL: 'http://localhost:5173',
|
||
wsURL: 'ws://localhost:3001/ws',
|
||
timeout: 30000,
|
||
adminUser: {
|
||
username: 'admin',
|
||
password: '111111',
|
||
},
|
||
};
|
||
|
||
test.describe('WebSocket 连接管理测试', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
test.setTimeout(TEST_CONFIG.timeout);
|
||
|
||
await page.goto(TEST_CONFIG.baseURL);
|
||
await loginAsAdmin(page);
|
||
});
|
||
|
||
test('WebSocket连接应该自动建立', async ({ page }) => {
|
||
// 等待WebSocket连接建立
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 检查连接状态指示器
|
||
const connectionStatus = page.locator(
|
||
'[data-testid="ws-connection-status"]',
|
||
);
|
||
await expect(connectionStatus).toBeVisible();
|
||
await expect(connectionStatus).toContainText(/已连接|Connected/);
|
||
|
||
// 检查连接状态图标
|
||
const statusIcon = page.locator('[data-testid="ws-status-icon"]');
|
||
await expect(statusIcon).toHaveClass(/connected|success/);
|
||
});
|
||
|
||
test('WebSocket断线重连应该正常工作', async ({ page }) => {
|
||
// 等待连接建立
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 模拟网络中断(通过开发者工具)
|
||
await page.evaluate(() => {
|
||
// 关闭WebSocket连接
|
||
if (window.wsConnection) {
|
||
window.wsConnection.close();
|
||
}
|
||
});
|
||
|
||
// 检查连接状态变为断开
|
||
const connectionStatus = page.locator(
|
||
'[data-testid="ws-connection-status"]',
|
||
);
|
||
await expect(connectionStatus).toContainText(/连接中断|Disconnected/, {
|
||
timeout: 5000,
|
||
});
|
||
|
||
// 等待自动重连
|
||
await page.waitForTimeout(5000);
|
||
|
||
// 检查是否重新连接
|
||
await expect(connectionStatus).toContainText(/已连接|Connected/, {
|
||
timeout: 10000,
|
||
});
|
||
});
|
||
|
||
test('手动重连功能应该正常工作', async ({ page }) => {
|
||
// 模拟连接断开
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
window.wsConnection.close();
|
||
}
|
||
});
|
||
|
||
// 等待状态更新
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 点击手动重连按钮
|
||
const reconnectButton = page.locator('[data-testid="ws-reconnect-button"]');
|
||
await reconnectButton.click();
|
||
|
||
// 检查重连状态
|
||
const connectionStatus = page.locator(
|
||
'[data-testid="ws-connection-status"]',
|
||
);
|
||
await expect(connectionStatus).toContainText(/连接中|Connecting/);
|
||
|
||
// 等待重连完成
|
||
await expect(connectionStatus).toContainText(/已连接|Connected/, {
|
||
timeout: 10000,
|
||
});
|
||
});
|
||
|
||
test('连接心跳机制应该正常工作', async ({ page }) => {
|
||
// 监听WebSocket消息
|
||
const heartbeatMessages: string[] = [];
|
||
|
||
await page.evaluateHandle(() => {
|
||
return new Promise((resolve) => {
|
||
const originalSend = WebSocket.prototype.send;
|
||
WebSocket.prototype.send = function (data) {
|
||
if (data.includes('heartbeat') || data.includes('ping')) {
|
||
window.heartbeatSent = true;
|
||
}
|
||
return originalSend.call(this, data);
|
||
};
|
||
|
||
setTimeout(resolve, 5000);
|
||
});
|
||
});
|
||
|
||
// 检查是否发送了心跳消息
|
||
const heartbeatSent = await page.evaluate(() => window.heartbeatSent);
|
||
expect(heartbeatSent).toBe(true);
|
||
});
|
||
});
|
||
|
||
test.describe('实时消息推送测试', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto(TEST_CONFIG.baseURL);
|
||
await loginAsAdmin(page);
|
||
|
||
// 等待WebSocket连接建立
|
||
await page.waitForTimeout(2000);
|
||
});
|
||
|
||
test('系统通知应该实时接收', async ({ page }) => {
|
||
// 监听通知消息
|
||
const notifications: any[] = [];
|
||
|
||
await page.exposeFunction('onNotification', (data: any) => {
|
||
notifications.push(data);
|
||
});
|
||
|
||
// 注册通知监听器
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
window.wsConnection.addEventListener('message', (event) => {
|
||
const data = JSON.parse(event.data);
|
||
if (data.type === 'notification') {
|
||
window.onNotification(data);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 模拟发送系统通知
|
||
await page.evaluate(() => {
|
||
// 这里应该触发一个会产生系统通知的操作
|
||
// 例如:创建一个新的任务或发送一条消息
|
||
});
|
||
|
||
// 等待通知到达
|
||
await page.waitForTimeout(3000);
|
||
|
||
// 检查通知是否显示
|
||
const notificationElement = page.locator('.ant-notification-notice');
|
||
if ((await notificationElement.count()) > 0) {
|
||
await expect(notificationElement).toBeVisible();
|
||
}
|
||
});
|
||
|
||
test('消息状态更新应该实时同步', async ({ page }) => {
|
||
// 导航到私信群发页面
|
||
await page.click('[data-testid="menu-private-message"]');
|
||
await page.click('[data-testid="menu-send-record"]');
|
||
await page.waitForURL(/\/private-message\/record/);
|
||
|
||
// 记录初始状态
|
||
const initialStats = await page
|
||
.locator('[data-testid="sending-count"]')
|
||
.textContent();
|
||
|
||
// 模拟消息状态变更(通过WebSocket)
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
// 模拟接收状态更新消息
|
||
const mockMessage = {
|
||
type: 'message_status_update',
|
||
data: {
|
||
taskId: 'test-task-123',
|
||
messageId: 'msg-456',
|
||
status: 'sent',
|
||
timestamp: Date.now(),
|
||
},
|
||
};
|
||
|
||
const event = new MessageEvent('message', {
|
||
data: JSON.stringify(mockMessage),
|
||
});
|
||
|
||
window.wsConnection.dispatchEvent(event);
|
||
}
|
||
});
|
||
|
||
// 等待状态更新
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 检查统计数据是否更新
|
||
const updatedStats = await page
|
||
.locator('[data-testid="sending-count"]')
|
||
.textContent();
|
||
|
||
// 至少UI应该是响应的
|
||
await expect(page.locator('[data-testid="sending-count"]')).toBeVisible();
|
||
});
|
||
|
||
test('用户在线状态应该实时更新', async ({ page }) => {
|
||
// 导航到用户列表页面
|
||
await page.click('[data-testid="menu-account"]');
|
||
await page.click('[data-testid="menu-account-list"]');
|
||
await page.waitForURL(/\/account\/list/);
|
||
|
||
// 检查用户在线状态指示器
|
||
const onlineIndicators = page.locator('[data-testid="user-online-status"]');
|
||
|
||
if ((await onlineIndicators.count()) > 0) {
|
||
// 模拟用户状态变更
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
const mockMessage = {
|
||
type: 'user_status_update',
|
||
data: {
|
||
userId: 'user-123',
|
||
status: 'online',
|
||
timestamp: Date.now(),
|
||
},
|
||
};
|
||
|
||
const event = new MessageEvent('message', {
|
||
data: JSON.stringify(mockMessage),
|
||
});
|
||
|
||
window.wsConnection.dispatchEvent(event);
|
||
}
|
||
});
|
||
|
||
// 等待状态更新
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 检查状态指示器
|
||
await expect(onlineIndicators.first()).toBeVisible();
|
||
}
|
||
});
|
||
|
||
test('任务进度应该实时更新', async ({ page }) => {
|
||
// 导航到任务列表页面
|
||
await page.click('[data-testid="menu-private-message"]');
|
||
await page.click('[data-testid="menu-send-task"]');
|
||
await page.waitForURL(/\/private-message\/task/);
|
||
|
||
// 找到一个进行中的任务
|
||
const taskProgress = page.locator('[data-testid="task-progress"]').first();
|
||
|
||
if ((await taskProgress.count()) > 0) {
|
||
// 记录初始进度
|
||
const initialProgress = await taskProgress.getAttribute('data-percent');
|
||
|
||
// 模拟进度更新
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
const mockMessage = {
|
||
type: 'task_progress_update',
|
||
data: {
|
||
taskId: 'task-123',
|
||
progress: 75,
|
||
completed: 750,
|
||
total: 1000,
|
||
timestamp: Date.now(),
|
||
},
|
||
};
|
||
|
||
const event = new MessageEvent('message', {
|
||
data: JSON.stringify(mockMessage),
|
||
});
|
||
|
||
window.wsConnection.dispatchEvent(event);
|
||
}
|
||
});
|
||
|
||
// 等待进度更新
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 检查进度条是否更新
|
||
await expect(taskProgress).toBeVisible();
|
||
}
|
||
});
|
||
});
|
||
|
||
test.describe('实时监控功能测试', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto(TEST_CONFIG.baseURL);
|
||
await loginAsAdmin(page);
|
||
|
||
// 导航到实时监控页面
|
||
await page.click('[data-testid="menu-private-message"]');
|
||
await page.click('[data-testid="menu-monitor"]');
|
||
await page.waitForURL(/\/private-message\/monitor/);
|
||
});
|
||
|
||
test('实时性能指标应该正常更新', async ({ page }) => {
|
||
// 检查性能指标卡片
|
||
const performanceMetrics = [
|
||
'[data-testid="cpu-usage"]',
|
||
'[data-testid="memory-usage"]',
|
||
'[data-testid="active-connections"]',
|
||
'[data-testid="message-rate"]',
|
||
];
|
||
|
||
for (const metric of performanceMetrics) {
|
||
await expect(page.locator(metric)).toBeVisible();
|
||
}
|
||
|
||
// 记录初始值
|
||
const initialCpu = await page
|
||
.locator('[data-testid="cpu-usage"]')
|
||
.textContent();
|
||
|
||
// 等待数据更新
|
||
await page.waitForTimeout(5000);
|
||
|
||
// 检查数据是否可能发生变化
|
||
const currentCpu = await page
|
||
.locator('[data-testid="cpu-usage"]')
|
||
.textContent();
|
||
|
||
// 至少UI应该是响应的
|
||
await expect(page.locator('[data-testid="cpu-usage"]')).toBeVisible();
|
||
});
|
||
|
||
test('实时日志应该正常显示', async ({ page }) => {
|
||
// 检查日志显示区域
|
||
await expect(page.locator('.realtime-logs')).toBeVisible();
|
||
|
||
// 检查日志条目
|
||
const logEntries = page.locator('.log-entry');
|
||
|
||
// 如果有日志条目,检查其结构
|
||
if ((await logEntries.count()) > 0) {
|
||
const firstLog = logEntries.first();
|
||
await expect(firstLog.locator('.log-timestamp')).toBeVisible();
|
||
await expect(firstLog.locator('.log-level')).toBeVisible();
|
||
await expect(firstLog.locator('.log-message')).toBeVisible();
|
||
}
|
||
|
||
// 模拟新日志产生
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
const mockLogMessage = {
|
||
type: 'log_message',
|
||
data: {
|
||
level: 'info',
|
||
message: '测试日志消息',
|
||
timestamp: Date.now(),
|
||
source: 'test-component',
|
||
},
|
||
};
|
||
|
||
const event = new MessageEvent('message', {
|
||
data: JSON.stringify(mockLogMessage),
|
||
});
|
||
|
||
window.wsConnection.dispatchEvent(event);
|
||
}
|
||
});
|
||
|
||
// 等待新日志显示
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 检查是否有新的日志条目
|
||
await expect(page.locator('.log-entry')).toHaveCount({ min: 1 });
|
||
});
|
||
|
||
test('告警信息应该实时显示', async ({ page }) => {
|
||
// 检查告警区域
|
||
const alertsSection = page.locator('.alerts-section');
|
||
await expect(alertsSection).toBeVisible();
|
||
|
||
// 模拟告警消息
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
const mockAlert = {
|
||
type: 'alert',
|
||
data: {
|
||
level: 'warning',
|
||
title: '发送频率过高',
|
||
message: '检测到发送频率超过限制,请注意调整',
|
||
timestamp: Date.now(),
|
||
id: 'alert-' + Date.now(),
|
||
},
|
||
};
|
||
|
||
const event = new MessageEvent('message', {
|
||
data: JSON.stringify(mockAlert),
|
||
});
|
||
|
||
window.wsConnection.dispatchEvent(event);
|
||
}
|
||
});
|
||
|
||
// 等待告警显示
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 检查告警是否显示
|
||
const alertItems = page.locator('.alert-item');
|
||
if ((await alertItems.count()) > 0) {
|
||
await expect(alertItems.first()).toBeVisible();
|
||
await expect(alertItems.first().locator('.alert-title')).toContainText(
|
||
'发送频率过高',
|
||
);
|
||
}
|
||
});
|
||
|
||
test('实时图表应该正常更新', async ({ page }) => {
|
||
// 检查实时图表容器
|
||
const chartContainer = page.locator('.realtime-chart .echarts-container');
|
||
await expect(chartContainer).toBeVisible();
|
||
|
||
// 等待图表渲染
|
||
await page.waitForTimeout(3000);
|
||
|
||
// 检查图表是否有数据
|
||
const chartCanvas = chartContainer.locator('canvas').first();
|
||
await expect(chartCanvas).toBeVisible();
|
||
|
||
// 模拟新的性能数据
|
||
await page.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
const mockPerformanceData = {
|
||
type: 'performance_data',
|
||
data: {
|
||
timestamp: Date.now(),
|
||
cpu: Math.random() * 100,
|
||
memory: Math.random() * 100,
|
||
connections: Math.floor(Math.random() * 1000),
|
||
messageRate: Math.floor(Math.random() * 500),
|
||
},
|
||
};
|
||
|
||
const event = new MessageEvent('message', {
|
||
data: JSON.stringify(mockPerformanceData),
|
||
});
|
||
|
||
window.wsConnection.dispatchEvent(event);
|
||
}
|
||
});
|
||
|
||
// 等待图表更新
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 图表应该仍然可见且响应
|
||
await expect(chartCanvas).toBeVisible();
|
||
});
|
||
});
|
||
|
||
test.describe('多标签页同步测试', () => {
|
||
test('多标签页状态应该保持同步', async ({ context }) => {
|
||
// 创建两个标签页
|
||
const page1 = await context.newPage();
|
||
const page2 = await context.newPage();
|
||
|
||
// 在两个标签页都登录
|
||
await page1.goto(TEST_CONFIG.baseURL);
|
||
await loginAsAdmin(page1);
|
||
|
||
await page2.goto(TEST_CONFIG.baseURL);
|
||
await loginAsAdmin(page2);
|
||
|
||
// 等待WebSocket连接建立
|
||
await Promise.all([page1.waitForTimeout(2000), page2.waitForTimeout(2000)]);
|
||
|
||
// 在第一个标签页触发一个操作
|
||
await page1.click('[data-testid="menu-private-message"]');
|
||
await page1.click('[data-testid="menu-send-task"]');
|
||
|
||
// 模拟在第一个标签页创建任务
|
||
await page1.evaluate(() => {
|
||
if (window.wsConnection) {
|
||
const mockTaskCreated = {
|
||
type: 'task_created',
|
||
data: {
|
||
taskId: 'new-task-123',
|
||
name: '新创建的测试任务',
|
||
timestamp: Date.now(),
|
||
},
|
||
};
|
||
|
||
const event = new MessageEvent('message', {
|
||
data: JSON.stringify(mockTaskCreated),
|
||
});
|
||
|
||
window.wsConnection.dispatchEvent(event);
|
||
}
|
||
});
|
||
|
||
// 切换到第二个标签页的相同页面
|
||
await page2.click('[data-testid="menu-private-message"]');
|
||
await page2.click('[data-testid="menu-send-task"]');
|
||
|
||
// 等待数据同步
|
||
await page2.waitForTimeout(3000);
|
||
|
||
// 检查两个标签页的数据是否一致
|
||
const page1TaskCount = await page1.locator('.task-item').count();
|
||
const page2TaskCount = await page2.locator('.task-item').count();
|
||
|
||
// 至少UI应该是同步的
|
||
expect(Math.abs(page1TaskCount - page2TaskCount)).toBeLessThanOrEqual(1);
|
||
|
||
await page1.close();
|
||
await page2.close();
|
||
});
|
||
});
|
||
|
||
// 辅助函数:管理员登录
|
||
async function loginAsAdmin(page: Page) {
|
||
await page.fill(
|
||
'[data-testid="username-input"]',
|
||
TEST_CONFIG.adminUser.username,
|
||
);
|
||
await page.fill(
|
||
'[data-testid="password-input"]',
|
||
TEST_CONFIG.adminUser.password,
|
||
);
|
||
await page.click('[data-testid="login-button"]');
|
||
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
|
||
}
|