| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 |
- // Isolated browser acceptance against local builds and fixtures; no online writes.
- import {chromium} from 'playwright';
- import {createServer} from 'node:http';
- import {readFileSync,existsSync,mkdirSync,writeFileSync} from 'node:fs';
- import {resolve,extname} from 'node:path';
- import assert from 'node:assert/strict';
- const output=resolve('tmp/support-requests-qa');mkdirSync(output,{recursive:true});
- async function serve(project){
- const root=resolve('dist/'+project+'/browser');
- const server=createServer((req,res)=>{let file=resolve(root,'.'+new URL(req.url,'http://local').pathname);if(!file.startsWith(root+'/')||!existsSync(file)||!extname(file))file=root+'/index.html';res.setHeader('Content-Type',({'.html':'text/html','.js':'text/javascript','.css':'text/css','.svg':'image/svg+xml','.woff2':'font/woff2'})[extname(file)]||'application/octet-stream');res.end(readFileSync(file));});
- await new Promise(r=>server.listen(0,'127.0.0.1',r));return {server,origin:'http://127.0.0.1:'+server.address().port};
- }
- const admin=await serve('xiaoshu-admin'),mobile=await serve('xiaoshu-mobile');
- const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
- const errors=[],checks=[],calls=[];let fail=false;
- const records=Array.from({length:25},(_,i)=>({id:'request'+i,userId:String(i%2+1),accountName:i%2?'张老师':'李同学',title:(i%3===0?'【休假申请】':i%3===1?'【意见反馈】':'历史提交 ')+(i%2?'张老师':'李同学'),content:i===0?'10月16日至17日因事申请休假。\n请管理员查看并协调安排。':i===1?'<script>window.injected=true</script>\n希望优化课程提醒。':'希望调整上课时间,感谢老师的帮助。',submittedAt:'2026-10-15T04:30:00Z',kind:['leave','feedback','legacy'][i%3],role:i%2?'teacher':'student',status:'pending',response:'',version:'fixture-version',unread:false}));
- try{
- const context=await browser.newContext({viewport:{width:1440,height:1000}});
- await context.addInitScript(()=>{const auth={objectId:'fixture-admin',sessionToken:'fixture-only',username:'验收管理员',operationsRole:'ops-manager'};localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth));});
- await context.route('**/*',async route=>{
- const u=new URL(route.request().url());if(u.origin!==admin.origin)return route.abort();if(!u.pathname.startsWith('/cloud-functions/'))return route.continue();
- const {params:p}=route.request().postDataJSON();calls.push(p);
- if(p.operation==='ops/support-requests/process'){const row=records.find(r=>r.id===p.targetId);assert.ok(p.comment);assert.equal(p.version,'fixture-version');Object.assign(row,{status:p.status,response:p.comment,handledAt:new Date().toISOString(),actorName:'验收管理员',unread:true});return route.fulfill({json:{success:true,data:row}});}
- if(p.operation!=='ops/support-requests/list')return route.fulfill({json:{success:true,data:{}}});
- if(fail)return route.fulfill({status:500,json:{success:false,message:'模拟读取失败'}});
- const filtered=records.filter(r=>(!p.role||r.role===p.role)&&(!p.kind||r.kind===p.kind)&&(!p.search||(r.title+r.accountName+r.content).includes(p.search)));
- await route.fulfill({json:{success:true,data:{canProcess:true,items:filtered.slice((p.page-1)*20,p.page*20),total:filtered.length,page:p.page,pageSize:20,summary:{feedback:filtered.filter(r=>r.kind==='feedback').length,leave:filtered.filter(r=>r.kind==='leave').length,legacy:filtered.filter(r=>r.kind==='legacy').length},refreshedAt:'2026-10-15T04:45:00Z'}}});
- });
- const page=await context.newPage();page.setDefaultTimeout(12000);page.on('pageerror',e=>errors.push(e.message));await page.goto(admin.origin+'/admin/support-requests');await page.locator('.request-row').first().waitFor();
- assert.equal(await page.getByRole('link',{name:'反馈与休假申请',exact:true}).count(),1);
- assert.equal(await page.locator('.request-row').count(),20);assert.match(await page.locator('.summary-grid').innerText(),/25/);assert.equal(await page.evaluate(()=>window.injected),undefined);
- await page.screenshot({path:output+'/admin-desktop.png',fullPage:true});await page.screenshot({path:output+'/admin-overview.png'});
- await page.getByRole('button',{name:'下一页',exact:true}).click();await page.waitForFunction(()=>document.querySelectorAll('.request-row').length===5);checks.push('菜单入口、全部记录、分页及内容按纯文本显示');
- await page.getByLabel('提交身份').selectOption('teacher');await page.getByLabel('类型',{exact:true}).selectOption('leave');await page.getByRole('button',{name:'查询',exact:true}).click();await page.waitForFunction(()=>document.querySelectorAll('.request-row').length===4);
- assert.equal(calls.at(-1).page,1);assert.ok((await page.locator('.request-meta').allTextContents()).every(t=>t.includes('老师')&&t.includes('休假申请')));checks.push('身份与类型组合筛选,查询回到第一页');
- await page.getByLabel('搜索',{exact:true}).fill('不存在的内容');await page.getByRole('button',{name:'查询',exact:true}).click();await page.getByText('暂无符合条件的反馈或申请').waitFor();
- await page.getByRole('button',{name:'重置',exact:true}).click();await page.locator('.request-row').first().waitFor();
- fail=true;await page.getByRole('button',{name:'查询',exact:true}).click();await page.getByRole('alert').filter({hasText:'模拟读取失败'}).waitFor();assert.equal(await page.locator('.request-row').count(),0);
- fail=false;await page.getByRole('button',{name:'重新加载',exact:true}).click();await page.locator('.request-row').first().waitFor();checks.push('空状态、失败不伪装为空记录、重试恢复');
- for(const width of [768,390]){await page.setViewportSize({width,height:900});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1));await page.screenshot({path:output+'/admin-'+width+'.png',fullPage:true});}checks.push('1440/768/390宽度布局');
- for(const [index,decision,comment] of [[0,'approved','已同意休假,请提前完成交接。'],[3,'rejected','当天已有排课,请另行选择时间。'],[1,'processed','提醒设置已调整,感谢反馈。']]){
- const row=page.locator('.request-row').nth(index);await row.getByRole('button',{name:/处理申请|处理反馈/}).click();
- if(decision!=='processed')await row.getByLabel('处理结果').selectOption(decision);
- await row.getByLabel('处理意见(提交人可见)').fill(comment);
- await row.getByRole('button',{name:/并发送回复/}).click();await page.locator('.request-row').nth(index).locator('.resolution').waitFor();
- assert.ok((await page.locator('.request-row').nth(index).innerText()).includes(comment));
- }
- checks.push('后台处理意见反馈、同意及拒绝休假,提交处理意见并显示未读状态');
- await page.screenshot({path:output+'/admin-processed.png',fullPage:true});
- for(const role of [{id:1,group:1,name:'李同学',type:'feedback'},{id:2,group:3,name:'张老师',type:'leave'}]){
- const c=await browser.newContext({viewport:{width:390,height:844}});await c.addInitScript(role=>localStorage.setItem('xiaoshu.angular.session',JSON.stringify({token:'r:fixture-only',user:{userId:role.id,groupId:role.group,honeyName:role.name}})),role);
- let submitted;
- await c.route('**/*',async route=>{const u=new URL(route.request().url());if(u.origin!==mobile.origin)return route.abort();if(!u.pathname.startsWith('/cloud-functions/'))return route.continue();const {params:p}=route.request().postDataJSON();if(p.action==='app_account_overview')return route.fulfill({json:{retcode:1,result:{profile:{userId:role.id,groupId:role.group,honeyName:role.name},refreshedAt:new Date().toISOString()}}});if(p.action==='app_support_requests_list'){const own=records.filter(r=>Number(r.userId)===role.id);return route.fulfill({json:{retcode:1,result:{items:own.slice(0,10),total:own.length,unread:own.filter(r=>r.unread).length}}});}if(p.action==='app_support_request_read'){const row=records.find(r=>r.id===p.id);assert.equal(Number(row.userId),role.id);row.unread=false;return route.fulfill({json:{retcode:1,result:{read:true}}});}if(p.action==='guestbook_add'){submitted=JSON.parse(p.model);records.unshift({id:'submitted-'+role.id,userId:String(role.id),title:submitted.Title,content:submitted.TContent,kind:role.type,status:'pending',unread:false,submittedAt:new Date().toISOString()});return route.fulfill({json:{retcode:1,result:{GID:123}}});}return route.fulfill({json:{retcode:1,result:{valid:true}}});});
- const p=await c.newPage();p.setDefaultTimeout(12000);p.on('pageerror',e=>errors.push(e.message));await p.goto(mobile.origin+'/pages/home/ucenter');await p.getByRole('link',{name:/条新的处理回复/}).click();await p.getByRole('heading',{name:'我的提交与回复'}).waitFor();await p.getByRole('button',{name:'提交反馈 / 申请',exact:true}).click();await p.locator('#name').fill(role.name);await p.getByRole('combobox',{name:/提交类型/}).click();await p.getByRole('option',{name:role.type==='leave'?'休假申请':'意见反馈',exact:true}).click();await p.locator('#content').fill('浏览器提交验收');await p.screenshot({path:output+'/mobile-'+role.type+'.png',fullPage:true});await p.getByRole('button',{name:'提交',exact:true}).click();await p.getByText('提交成功,管理员处理后会在这里回复。',{exact:true}).waitFor();
- assert.equal(submitted.UserID,role.id);assert.equal(submitted.Cateid,22);assert.equal(submitted.Title,`【${role.type==='leave'?'休假申请':'意见反馈'}】${role.name}`);assert.equal(submitted.TContent,'浏览器提交验收');
- const reply=p.locator('.reply-card').filter({has:p.getByText('新回复',{exact:true})}).first();await reply.waitFor();
- const ownRecord=records.find(r=>Number(r.userId)===role.id&&r.unread);await reply.getByRole('button',{name:'查看处理意见'}).click();await p.locator('.response').waitFor();assert.ok((await p.locator('.response').innerText()).includes(ownRecord.response));
- assert.equal(ownRecord.unread,false);assert.equal(await p.evaluate(()=>window.injected),undefined);assert.ok(await p.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1));await p.screenshot({path:output+'/mobile-reply-'+role.type+'.png',fullPage:true});await c.close();
- }
- checks.push('学生和老师个人中心新回复提示、跳转我的记录、提交、查看本人处理意见及已读,390宽度无溢出');
- assert.deepEqual(errors,[]);writeFileSync(output+'/result.json',JSON.stringify({passed:true,checks,errors},null,2));console.log(JSON.stringify({passed:true,checks,output}));
- }finally{await browser.close();admin.server.close();mobile.server.close();}
|