check-teacher-earnings-browser.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. #!/usr/bin/env node
  2. // Isolated local fixture browser acceptance. Does not read or write online accounts.
  3. import {chromium} from 'playwright';
  4. import {createServer} from 'node:http';
  5. import {readFileSync,existsSync,mkdirSync,writeFileSync} from 'node:fs';
  6. import {resolve,extname} from 'node:path';
  7. import assert from 'node:assert/strict';
  8. const root=resolve('dist/xiaoshu-mobile/browser'),out=resolve('tmp/teacher-earnings-qa');mkdirSync(out,{recursive:true});
  9. const server=createServer((req,res)=>{let file=resolve(root,'.'+decodeURIComponent(new URL(req.url,'http://local').pathname));if(!file.startsWith(root+'/'))file=root+'/index.html';if(!existsSync(file)||!extname(file))file=root+'/index.html';try{res.setHeader('Content-Type',({'.html':'text/html','.js':'text/javascript','.css':'text/css','.png':'image/png','.svg':'image/svg+xml','.woff2':'font/woff2'})[extname(file)]||'application/octet-stream');res.end(readFileSync(file));}catch{res.statusCode=404;res.end();}});
  10. await new Promise(r=>server.listen(0,'127.0.0.1',r));const origin='http://127.0.0.1:'+server.address().port;
  11. const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
  12. const checks=[],errors=[],calls=[];
  13. const day=new Date().toLocaleDateString('sv-SE',{timeZone:'Asia/Shanghai'}),yesterday=new Date(Date.now()-86400000).toLocaleDateString('sv-SE',{timeZone:'Asia/Shanghai'});
  14. const tasks=[{GeneralID:102,xxjlid:900,kcid:801,yhid:100,honeyName:'测试学员',kc_title:'高中单词课',kywrq:day,kywsj:day+' 09:00',fxzt:0},{GeneralID:103,xxjlid:900,kcid:801,yhid:100,honeyName:'测试学员',kc_title:'高中单词课',kywrq:yesterday,kywsj:yesterday+' 09:00',fxzt:0}];
  15. let completed=false,failReview=false,incomeMode='total';
  16. try{
  17. const context=await browser.newContext({viewport:{width:390,height:844}});
  18. await context.addInitScript(()=>localStorage.setItem('xiaoshu.angular.session',JSON.stringify({token:'r:local-fixture-only',user:{userId:300,groupId:3,honeyName:'测试老师'},student:{userId:100,groupId:1,honeyName:'测试学员'}})));
  19. await context.route('**/*',async route=>{
  20. const u=new URL(route.request().url());if(u.origin!==origin)return route.abort();
  21. if(!u.pathname.startsWith('/cloud-functions/'))return route.continue();
  22. const {params:p}=route.request().postDataJSON();calls.push(p);let result={};
  23. if(p.action==='e_order_tongji')result={t30:1,t60:2,t_total:3,t_shichang:2.5,lessonIncome:120,reviewIncome:completed?3:2,totalIncome:completed?123:122,pendingPayCount:1};
  24. if(p.action==='e_order_tongji'&&incomeMode!=='total'){delete result.totalIncome;if(incomeMode==='legacy'){delete result.lessonIncome;delete result.reviewIncome;result.t_yongji='7800';}if(incomeMode==='zero'){result.lessonIncome=0;result.reviewIncome=0;}}
  25. if(p.action==='app_live_versions')result={version:String(completed),scopeVersions:{},refreshedAt:new Date().toISOString(),syncStatus:{state:'current'}};
  26. else if(p.action==='app_review_page'){
  27. if(failReview)return route.fulfill({status:500,json:{retcode:-1,retmsg:'模拟读取失败'}});
  28. const items=p.scope==='today'?tasks.slice(0,1):p.scope==='overdue'?tasks.slice(1):p.status==='completed'?tasks.filter(t=>t.fxzt===1):tasks.filter(t=>!p.date||t.kywrq===p.date);
  29. result={serverToday:day,items,page:{itemCount:items.length,pageCount:1},summary:{total:2,completed:completed?1:0,due:completed?1:2,today:1,todayTotal:1,todayCompleted:completed?1:0,todayPending:completed?0:1,overduePending:1,income:completed?3:2},dates:[{date:day,total:1,completed:completed?1:0},{date:yesterday,total:1,completed:0}],students:[{id:100,name:'测试学员',taskCount:2}]};
  30. }else if(p.action==='e_record_detail')result=[{GeneralID:1001,Title:'apple',sy:'苹果'}];
  31. else if(p.action==='app_review_complete'){const task=tasks.find(t=>t.GeneralID===Number(p.id));const unchanged=task.fxzt===1;task.fxzt=1;task.money=task.kywrq===day?1:0;task.rewardStatus=task.money?'earned':'ineligible';task.ineligibleReason=task.money?'':'overdue';if(task.money)completed=true;result={completed:true,amount:task.money,rewardStatus:task.rewardStatus,unchanged};}
  32. else if(p.action==='app_companion_overview')result={courses:[],schedules:[45,0,null].map((pay,i)=>({GeneralID:901+i,Title:'测试学员(单词课)',bxrq:day,sdsd:'10:00',dszt:30,teacherPay:pay,payStatus:pay===null?'pending':'earned',feedbackStatus:i?'submitted':'pending'})),dates:[{date:day,finished:3,planned:0}],page:{itemCount:3,pageCount:1},studentOptions:[],refreshedAt:new Date().toISOString()};
  33. else if(p.action==='app_account_overview')result={profile:{},coachPanel:{lessonIncome:120,reviewIncome:3,totalIncome:123,totalLessons:3,totalHours:2.5},refreshedAt:new Date().toISOString()};
  34. else if(p.action==='user_list')result=[];
  35. await route.fulfill({json:{retcode:1,result}});
  36. });
  37. const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));page.on('dialog',d=>d.accept());
  38. await page.goto(origin+'/pages/home/index');await page.locator('.coach-memory-list article').waitFor();
  39. assert.match(await page.locator('.commission').innerText(), /累计收入[\s\S]*\*\*\*/);
  40. assert.match(await page.locator('.coach-footer').innerText(), /累计时长 2\.5/);
  41. assert.equal(await page.locator('.income-breakdown').count(),0);
  42. assert.doesNotMatch(await page.locator('.coach-hero').innerText(), /累计带课工资|累计复习工资|已记账收入|不代表已发放|工资待核对/);
  43. await page.getByRole('button',{name:'显示累计收入'}).click();
  44. await page.waitForFunction(()=>document.querySelector('.commission')?.textContent.includes('¥122.00'));
  45. assert.match(await page.locator('.commission').innerText(), /¥122\.00/);
  46. await page.getByRole('button',{name:'隐藏累计收入'}).click();
  47. await page.waitForFunction(()=>document.querySelector('.commission')?.textContent.includes('***'));
  48. assert.doesNotMatch(await page.locator('.commission').innerText(), /¥122/);
  49. await page.getByRole('button',{name:'显示累计收入'}).click();
  50. await page.waitForFunction(()=>document.querySelector('.commission')?.textContent.includes('¥122.00'));
  51. assert.ok(calls.some(p=>p.action==='app_review_page'&&p.scope==='today'&&p.pageSize===100&&p.date===day&&p.includeMetadata===false));checks.push('首页当天范围、仅保留时长和总收入、默认隐藏且眼睛可切换');
  52. await page.screenshot({path:out+'/home-390.png',fullPage:true});
  53. await page.getByRole('button',{name:'查看计划',exact:true}).click();await page.locator('.record-list').waitFor();assert.equal(await page.getByRole('button',{name:'返回复习任务',exact:true}).count(),0);await page.screenshot({path:out+'/review-390.png',fullPage:true});await page.getByRole('button',{name:'返回',exact:true}).click();await page.locator('.coach-memory-list article').waitFor();assert.equal(calls.filter(p=>p.action==='app_review_page'&&p.scope==='today').length,1);await page.getByRole('button',{name:'查看计划',exact:true}).click();await page.locator('.record-list').waitFor();checks.push('复习页仅保留顶部返回,返回首页不重新加载任务');await page.getByRole('button',{name:'结束本次复习',exact:true}).click();await page.locator('.review-complete').waitFor();assert.match(await page.locator('.review-complete').innerText(),/本次收入 ¥1\.00/);await page.screenshot({path:out+'/review-complete-390.png',fullPage:true});
  54. await page.getByRole('link',{name:'返回首页',exact:true}).click();await page.getByText('今天的复习任务已全部完成').waitFor();checks.push('完成结果显示服务器金额、返回首页更新缓存计数且不重新请求');
  55. assert.equal(calls.filter(p=>p.action==='app_review_page'&&p.scope==='today').length,1);
  56. assert.equal(await page.getByText('查看今日全部任务').count(),0);
  57. await page.getByRole('link',{name:'查看全部学员复习计划'}).click();await page.locator('.memory-date-tasks').waitFor();assert.match(page.url(),/\/pages\/stu\/stu_calendar$/);
  58. await page.goto(origin+'/pages/stu/stu_calendar?scope=overdue');await page.locator('.memory-date-tasks').waitFor();assert.ok(await page.locator('.tabbar').isVisible());await page.getByRole('button',{name:'带学生复习',exact:false}).first().click();await page.getByText('这是逾期任务,可以补做,但不计复习工资。').waitFor();await page.getByRole('button',{name:'结束本次复习',exact:true}).click();await page.locator('.review-complete').waitFor();assert.match(await page.locator('.review-complete').innerText(),/本次收入 ¥0\.00/);checks.push('逾期入口、补做前说明、完成零元显示');
  59. await page.goto(origin+'/pages/member/ke_xiao');await page.locator('.lesson-pay').first().waitFor();assert.match(await page.locator('body').innerText(),/带课工资 ¥45\.00/);assert.match(await page.locator('body').innerText(),/带课工资 ¥0\.00/);assert.match(await page.locator('body').innerText(),/工资待核对/);assert.ok(await page.locator('.tabbar').isVisible());
  60. assert.equal(await page.locator('.lesson-pay small.feedback-pending').count(),1);assert.equal(await page.locator('.lesson-pay small.feedback-pending').evaluate(e=>getComputedStyle(e).color),'rgb(180, 83, 9)');
  61. for(const width of [390,360]){await page.setViewportSize({width,height:844});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1));assert.ok(await page.locator('.schedule-copy').first().evaluate(e=>e.getBoundingClientRect().width>=80));await page.screenshot({path:out+'/history-'+width+'.png',fullPage:true});}
  62. checks.push('销课45元、合法零元、待核对分开显示;360/390宽度无横向溢出且固定导航可见');
  63. for(const mode of ['legacy','split','zero']){
  64. incomeMode=mode;await page.goto(origin+'/pages/home/index');await page.locator('.coach-memory-list article').waitFor();
  65. await page.getByRole('button',{name:'刷新业务数据',exact:true}).click();
  66. await page.waitForFunction(()=>!document.querySelector('.coach-hero .coach-refresh')?.disabled);
  67. await page.getByRole('button',{name:'显示累计收入'}).click();
  68. const expected=mode==='legacy'?'暂不可用':mode==='zero'?'¥0.00':'¥123.00';
  69. await page.waitForFunction(value=>document.querySelector('.commission-amount')?.textContent?.trim()===value,expected);
  70. if(mode==='legacy')assert.doesNotMatch(await page.locator('.commission').innerText(),/7800/);
  71. }
  72. incomeMode='total';checks.push('旧接口缺金额明确显示不可用,不展示估算佣金;实际收入分项可合计,合法零元显示正常');
  73. failReview=true;await page.goto(origin+'/pages/home/index');await page.getByText('今天的复习任务已全部完成').waitFor();await page.getByRole('button',{name:'刷新抗遗忘任务',exact:true}).click();await page.getByText('抗遗忘计划暂时无法加载,点击重试').waitFor();assert.equal(await page.getByText('今天没有复习任务').count(),0);checks.push('接口失败不会显示没有任务');
  74. assert.deepEqual(errors,[]);writeFileSync(out+'/browser-result.json',JSON.stringify({passed:true,environment:'local build + isolated fixture API, not live backend or WeChat',checks,requests:calls.length,errors},null,2));console.log(JSON.stringify({passed:true,checks},null,2));
  75. }finally{await browser.close();await new Promise(r=>server.close(r));}