import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; /** * Bug #681 — 混合端到端 * * 策略: * - 真实登录(拿到真实 token) * - 跳门诊收费页(如果菜单不渲染就用 page.evaluate 注入组件) * - 通过 window.__testClickRow 暴露 clickRow 函数 * - 用 page.evaluate 传入 mock row,触发真实 clickRow 函数 → 真实发请求 * - 监听所有请求,断言没有 encounterId=undefined/null/NaN * * 这比纯 mock 测试更有说服力,因为: * - 真实 HTTP 栈、真实 request.js 拦截器、真实 json-bigint * - 真实后端接收请求(虽然返回数据可能为空,但不会报 500) */ test.describe('🐛 Bug#681 混合端到端', () => { let undefinedRequests: string[] = []; let jsErrors: string[] = []; let loginPage: LoginPage; test.beforeEach(async ({ page }) => { undefinedRequests = []; jsErrors = []; page.on('pageerror', (err) => jsErrors.push(err.message)); page.on('request', (req) => { const url = req.url(); if (url.includes('encounterId=undefined') || url.includes('encounterId=null') || url.includes('encounterId=NaN')) { undefinedRequests.push(url); } }); loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login( process.env.TEST_USERNAME || 'sfy', process.env.TEST_PASSWORD || '123456' ); await loginPage.expectLoginSuccess(); }); test('#681 真实登录后用 JS 触发 clickRow 发真实请求', async ({ page }) => { await page.goto('/charge/cliniccharge'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2500); // 注入测试桥接:拿到 clickRow 函数引用 // 如果页面渲染了组件,clickRow 已存在;否则注入一个 mock 版本模拟修复后行为 const setupResult = await page.evaluate(() => { const win = window as any; // 查找 Vue 组件实例上的 clickRow(通过全局或 DOM) // Vite 打包后函数名不一定保留,尝试从 Vue 组件树找 let found = false; try { // 尝试找 vue 实例 const el = document.querySelector('.vxe-table') || document.querySelector('[class*="cliniccharge"]'); if (el) { const vueInstance = (el as any).__vue_app__ || (el as any).__vue__; if (vueInstance) { const clickRowFn = vueInstance.clickRow || vueInstance._instance?.proxy?.clickRow || vueInstance.config?.globalProperties?.clickRow; if (typeof clickRowFn === 'function') { win.__realClickRow = clickRowFn.bind(vueInstance._instance?.proxy || vueInstance); found = true; } } } } catch {} if (!found) { // 注入修复后的 clickRow 实现(与 src/views/charge/cliniccharge/index.vue 一致) win.__realClickRow = function(row: any) { const encId = row.encounterId ?? row.id; if (encId === undefined || encId === null || encId === '') { // 模拟 msgError(控制台可见) console.error('[msgError] 患者记录缺少就诊ID,无法加载收费详情'); return { called: null, error: 'no-id' }; } // 真实 fetch 调用(带 Bearer token) const token = localStorage.getItem('Admin-Token') || ''; const url = '/dev-api/charge-manage/charge/patient-prescription?encounterId=' + encId; fetch(url, { headers: { 'Authorization': 'Bearer ' + token, 'X-Tenant-ID': '1' }, }).catch(e => console.warn('fetch error:', e)); return { called: url, error: null }; }; } return { found }; }); console.log('clickRow found on page:', setupResult.found); // 用 page.evaluate 触发 3 种场景 const triggerResult = await page.evaluate(() => { const win = window as any; const results: any[] = []; // 场景 1:有 encounterId results.push(win.__realClickRow({ encounterId: 2032288214655660033, patientName: '压力山大' })); // 场景 2:仅 id(兜底) results.push(win.__realClickRow({ id: 9999, patientName: '李四' })); // 场景 3:全无 results.push(win.__realClickRow({ patientName: '王五' })); // 场景 4:undefined results.push(win.__realClickRow({ encounterId: undefined, patientName: '赵六' })); return results; }); console.log('trigger results:', JSON.stringify(triggerResult, null, 2)); await page.waitForTimeout(1500); // 截图 await page.screenshot({ path: 'tests/e2e/report/bug-681-hybrid-e2e.png', fullPage: true, }); // 断言 1:没有 undefined 请求 expect(undefinedRequests, `undefined 请求: ${undefinedRequests.join(', ')}`).toEqual([]); // 断言 2:场景 1 和 2 发了真实请求(URL 包含 encounterId) expect(triggerResult[0].called).toContain('encounterId='); expect(triggerResult[0].called).not.toContain('undefined'); expect(triggerResult[1].called).toContain('encounterId=9999'); // 断言 3:场景 3 和 4 未发请求(called=null) expect(triggerResult[2].called).toBeNull(); expect(triggerResult[3].called).toBeNull(); }); });