import {createRequire} from 'node:module'; import {writeFileSync} from 'node:fs'; const require=createRequire('C:/kefu/admin-web/package.json'); const {chromium}=require('playwright'); const root='C:/kefu/deploy/protocol-integration-20260916/qa'; const browser=await chromium.launch({executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe',headless:true,args:['--no-proxy-server']}); const context=await browser.newContext({viewport:{width:1600,height:1050},locale:'zh-CN'}); await context.route('**/*',route=>new URL(route.request().url()).hostname==='127.0.0.1'?route.continue():route.abort()); const page=await context.newPage();page.setDefaultTimeout(20000); const errors=[],checks=[],searches=[]; page.on('pageerror',error=>errors.push(error.message)); page.on('request',request=>{if(request.method()==='POST'&&request.url().includes('/knowledge/search'))searches.push(request.postDataJSON());}); const account=async name=>{await page.locator('.ant-card-extra .ant-select-selector').click();await page.locator('.ant-select-dropdown:visible').getByText(name,{exact:true}).click();}; const query=page.getByPlaceholder(/输入一个真实业务问题/); const search=page.getByRole('button',{name:/^检\s*索$/}); const assert=(value,text)=>{if(!value)throw Error(text);}; try{ await page.goto('http://127.0.0.1:18871/#/auth/login'); await page.getByPlaceholder('用户名',{exact:true}).fill('admin'); await page.getByPlaceholder('密码',{exact:true}).fill('KnowledgeQA@2026'); await page.getByRole('button',{name:'login',exact:true}).click(); await page.waitForURL(url=>!url.hash.includes('/auth/login')); await page.goto('http://127.0.0.1:18871/#/knowledge/center'); await account('检索验收客服(SQ001)'); await page.getByRole('tab',{name:'检索测试',exact:true}).click(); if(process.argv.includes('--before')){ await search.click();await page.getByText('[object Object]',{exact:true}).waitFor(); await page.screenshot({path:`${root}/00-before-object-error.png`,fullPage:true}); console.log('Reproduced: empty search displays [object Object]'); }else{ for(const value of ['', ' ', '约', ' 约 ']){ await query.fill(value);await search.click(); await page.getByText('请输入至少 2 个字符的检索问题',{exact:true}).waitFor(); } await query.fill('约'.repeat(2001));await search.click(); await page.getByText('检索问题不能超过 2000 个字符',{exact:true}).waitFor(); assert(searches.length===0,'Invalid searches reached API'); checks.push('空白、单字和超长问题在页面提示,不发送无效请求'); await query.fill(' 预约挂号 ');await query.press('Enter'); await page.getByText(/关键词检索.*1 条命中/).waitFor(); assert(searches[0].query==='预约挂号','Query not trimmed'); await page.screenshot({path:`${root}/01-success.png`,fullPage:true}); checks.push('正常问题支持回车检索并去除首尾空白,只返回 1 条已发布知识,不命中草稿或待发布'); await context.route('**/api/knowledge/search**',async route=>{ await route.fulfill({status:422,contentType:'application/json',body:JSON.stringify({detail:[{type:'string_too_short',loc:['body','query'],msg:'String should have at least 2 characters',input:'PRIVATE-MUST-NOT-RENDER',ctx:{min_length:2}}]})}); },{times:1}); await query.fill('校验错误模拟');await search.click(); await page.locator('.ant-alert').filter({hasText:'检索问题至少需要 2 个字符'}).waitFor(); let text=await page.locator('body').innerText();assert(!text.includes('[object Object]')&&!text.includes('PRIVATE-MUST-NOT-RENDER'),'Unsafe error display'); assert(!text.includes('1 条命中'),'Old result left after failed search'); await page.screenshot({path:`${root}/02-readable-error.png`,fullPage:true}); checks.push('后端 422 数组错误显示中文说明,无对象字符串、不回显请求内容,失败后清除旧结果'); await account('空知识验收客服(SQ002)'); await page.getByText('当前账号暂无已发布知识,请先审核并发布知识后再测试检索。',{exact:true}).waitFor(); await query.fill('预约挂号');await search.click(); await page.getByText('当前账号没有已发布知识,审核通过后还需发布才能检索。',{exact:true}).waitFor(); await page.screenshot({path:`${root}/03-no-published-knowledge.png`,fullPage:true}); checks.push('无已发布知识的账号正常返回 0 命中,并提示先发布'); assert(!errors.length,JSON.stringify(errors)); const result={passed:true,checks,pageErrors:errors};writeFileSync(`${root}/result.json`,JSON.stringify(result,null,2));console.log(JSON.stringify(result)); } }catch(error){await page.screenshot({path:`${root}/failure.png`,fullPage:true});writeFileSync(`${root}/failure.txt`,String(error)+'\n'+await page.locator('body').innerText());throw error;} finally{await browser.close();}