| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 |
- #!/usr/bin/env node
- // Isolated local fixture browser acceptance. Does not read or write online accounts.
- 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 root=resolve('dist/xiaoshu-mobile/browser'),out=resolve('tmp/teacher-earnings-qa');mkdirSync(out,{recursive:true});
- 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();}});
- await new Promise(r=>server.listen(0,'127.0.0.1',r));const origin='http://127.0.0.1:'+server.address().port;
- const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
- const checks=[],errors=[],calls=[];
- const day=new Date().toLocaleDateString('sv-SE',{timeZone:'Asia/Shanghai'}),yesterday=new Date(Date.now()-86400000).toLocaleDateString('sv-SE',{timeZone:'Asia/Shanghai'});
- 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}];
- let completed=false,failReview=false,incomeMode='total';
- try{
- const context=await browser.newContext({viewport:{width:390,height:844}});
- 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:'测试学员'}})));
- await context.route('**/*',async route=>{
- const u=new URL(route.request().url());if(u.origin!==origin)return route.abort();
- if(!u.pathname.startsWith('/cloud-functions/'))return route.continue();
- const {params:p}=route.request().postDataJSON();calls.push(p);let result={};
- 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};
- 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;}}
- if(p.action==='app_live_versions')result={version:String(completed),scopeVersions:{},refreshedAt:new Date().toISOString(),syncStatus:{state:'current'}};
- else if(p.action==='app_review_page'){
- if(failReview)return route.fulfill({status:500,json:{retcode:-1,retmsg:'模拟读取失败'}});
- 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);
- 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}]};
- }else if(p.action==='e_record_detail')result=[{GeneralID:1001,Title:'apple',sy:'苹果'}];
- 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};}
- 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()};
- 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()};
- else if(p.action==='user_list')result=[];
- await route.fulfill({json:{retcode:1,result}});
- });
- const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));page.on('dialog',d=>d.accept());
- await page.goto(origin+'/pages/home/index');await page.locator('.coach-memory-list article').waitFor();
- assert.match(await page.locator('.commission').innerText(), /累计收入[\s\S]*\*\*\*/);
- assert.match(await page.locator('.coach-footer').innerText(), /累计时长 2\.5/);
- assert.equal(await page.locator('.income-breakdown').count(),0);
- assert.doesNotMatch(await page.locator('.coach-hero').innerText(), /累计带课工资|累计复习工资|已记账收入|不代表已发放|工资待核对/);
- await page.getByRole('button',{name:'显示累计收入'}).click();
- await page.waitForFunction(()=>document.querySelector('.commission')?.textContent.includes('¥122.00'));
- assert.match(await page.locator('.commission').innerText(), /¥122\.00/);
- await page.getByRole('button',{name:'隐藏累计收入'}).click();
- await page.waitForFunction(()=>document.querySelector('.commission')?.textContent.includes('***'));
- assert.doesNotMatch(await page.locator('.commission').innerText(), /¥122/);
- await page.getByRole('button',{name:'显示累计收入'}).click();
- await page.waitForFunction(()=>document.querySelector('.commission')?.textContent.includes('¥122.00'));
- assert.ok(calls.some(p=>p.action==='app_review_page'&&p.scope==='today'&&p.pageSize===100&&p.date===day&&p.includeMetadata===false));checks.push('首页当天范围、仅保留时长和总收入、默认隐藏且眼睛可切换');
- await page.screenshot({path:out+'/home-390.png',fullPage:true});
- 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});
- await page.getByRole('link',{name:'返回首页',exact:true}).click();await page.getByText('今天的复习任务已全部完成').waitFor();checks.push('完成结果显示服务器金额、返回首页更新缓存计数且不重新请求');
- assert.equal(calls.filter(p=>p.action==='app_review_page'&&p.scope==='today').length,1);
- assert.equal(await page.getByText('查看今日全部任务').count(),0);
- await page.getByRole('link',{name:'查看全部学员复习计划'}).click();await page.locator('.memory-date-tasks').waitFor();assert.match(page.url(),/\/pages\/stu\/stu_calendar$/);
- 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('逾期入口、补做前说明、完成零元显示');
- 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());
- 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)');
- 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});}
- checks.push('销课45元、合法零元、待核对分开显示;360/390宽度无横向溢出且固定导航可见');
- for(const mode of ['legacy','split','zero']){
- incomeMode=mode;await page.goto(origin+'/pages/home/index');await page.locator('.coach-memory-list article').waitFor();
- await page.getByRole('button',{name:'刷新业务数据',exact:true}).click();
- await page.waitForFunction(()=>!document.querySelector('.coach-hero .coach-refresh')?.disabled);
- await page.getByRole('button',{name:'显示累计收入'}).click();
- const expected=mode==='legacy'?'暂不可用':mode==='zero'?'¥0.00':'¥123.00';
- await page.waitForFunction(value=>document.querySelector('.commission-amount')?.textContent?.trim()===value,expected);
- if(mode==='legacy')assert.doesNotMatch(await page.locator('.commission').innerText(),/7800/);
- }
- incomeMode='total';checks.push('旧接口缺金额明确显示不可用,不展示估算佣金;实际收入分项可合计,合法零元显示正常');
- 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('接口失败不会显示没有任务');
- 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));
- }finally{await browser.close();await new Promise(r=>server.close(r));}
|