如何将Playwright长时运行测试的进度推送至前端?
Node.js SSE推送Playwright测试进度失效的解决办法
问题背景
我在Node.js后端用SSE定义了测试进度路由,前端已初始化对应URL。后端路由代码如下:
let clients: Array<any> = []; router.get('/progress', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', }); const data = `data: ${JSON.stringify('Starting the test')}\n\n`; res.write(data); const clientId = Date.now(); const newClient = { id: clientId, res, }; clients.push(newClient); req.on('close', () => { console.log(`${clientId} Connection closed`); clients = clients.filter((client) => client.id !== clientId); }); });
前端点击按钮会触发后端运行Playwright长时测试(需遍历CSV文件检查),我想在测试执行过程中向前端推送进度,但把路由文件里的sendEventsToAll函数导入测试代码调用后完全没效果:
export function sendEventsToAll(data: any) { console.log('SSE ' + data); clients.forEach((client) => client.res.write(`data: ${JSON.stringify(data)}\n\n`)); }
最佳解决方案
问题根源
核心问题是模块引用导致的clients数组实例不统一:如果测试代码和路由文件是独立模块,直接导入的clients是新的引用,不是路由里实际维护的客户端列表,自然推不出消息。
解决步骤
1. 抽离SSE客户端管理为单例模块
新建一个独立的文件(比如sse-client-manager.ts),把客户端列表和操作方法封装在这里,确保所有模块共享同一个实例:
let clients: Array<{ id: number; res: any }> = []; // 添加新客户端 export function addClient(client: { id: number; res: any }) { clients.push(client); } // 移除断开的客户端 export function removeClient(clientId: number) { clients = clients.filter(client => client.id !== clientId); } // 向所有客户端推送消息 export function sendEventsToAll(data: any) { console.log('SSE 推送内容:', data); clients.forEach(client => { // 增加错误处理,避免客户端断开后写入报错 try { client.res.write(`data: ${JSON.stringify(data)}\n\n`); // 强制刷新响应,确保消息即时发送 client.res.flush(); } catch (err) { console.error(`推送失败给客户端${client.id}:`, err); removeClient(client.id); } }); }
2. 修改路由文件,使用单例模块的方法
把原来路由里的客户端操作替换为单例模块的方法:
import { addClient, removeClient, sendEventsToAll } from './sse-client-manager'; router.get('/progress', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', }); // 发送初始化消息 const initMsg = `data: ${JSON.stringify('Starting the test')}\n\n`; res.write(initMsg); res.flush(); const clientId = Date.now(); const newClient = { id: clientId, res }; addClient(newClient); // 客户端断开时移除 req.on('close', () => { console.log(`${clientId} 连接已关闭`); removeClient(clientId); }); });
3. 在Playwright测试代码中调用单例推送方法
现在测试代码里导入同一个单例模块的sendEventsToAll,就能正确推送给所有连接的客户端了:
import { sendEventsToAll } from './sse-client-manager'; // 示例Playwright测试逻辑 async function runPlaywrightCsvTest() { // 假设加载CSV数据 const csvRows = await loadTestCsv(); const total = csvRows.length; for (let index = 0; index < total; index++) { const row = csvRows[index]; // 执行单条数据的Playwright检查 await runSingleRowCheck(row); // 构造进度数据并推送 const progress = { current: index + 1, total, message: `已完成第 ${index+1}/${total} 条数据检查`, timestamp: new Date().toLocaleTimeString() }; sendEventsToAll(progress); // 可选:避免推送过于频繁,添加小延迟 await new Promise(resolve => setTimeout(resolve, 150)); } // 测试完成推送结束消息 sendEventsToAll({ completed: true, message: '所有CSV数据检查完成', timestamp: new Date().toLocaleTimeString() }); }
更优替代实现方式
1. Redis Pub/Sub(适合多实例部署)
如果你的后端是多实例集群,内存中的clients数组无法跨实例共享,这时候用Redis的发布订阅解耦:
- SSE路由启动时订阅Redis的
test-progress频道,收到消息后推送给当前客户端 - Playwright测试代码向
test-progress频道发布进度消息
这样不管测试在哪个实例运行,所有SSE客户端都能收到进度。
2. WebSocket(适合双向交互场景)
如果后续需要前端控制测试(比如暂停、终止),WebSocket比SSE更灵活——它支持双向通信。但如果只是单向推送进度,SSE更轻量、实现更简单。
3. 任务队列+状态查询(适合超长时间任务)
如果测试任务耗时极长(比如几小时),可以把任务放入队列(比如BullMQ),前端通过轮询或者SSE订阅任务状态,后端任务执行时更新队列中的状态。这种方式更稳定,还能支持任务重启、断点续跑。
内容的提问来源于stack exchange,提问作者Zuhair
相关产品推荐
相关产品推荐

