| 1234567891011121314 |
- const {chromium}=require(process.env.PLAYWRIGHT_MODULE||'playwright');const fs=require('fs');const assert=require('assert');
- (async()=>{const browser=await chromium.launch({headless:true,channel:'chrome'});const results=[],errors=[];
- const root='docs/qa/launch-fixes-2026-10-13';let changes=0;
- const p=await browser.newPage({viewport:{width:375,height:812}});p.on('pageerror',e=>errors.push(e.message));
- await p.route('**/*',async r=>{const u=new URL(r.request().url());if(!['127.0.0.1','localhost'].includes(u.hostname))return r.abort();if(u.pathname.startsWith('/cloud-functions')){const params=r.request().postDataJSON()?.params||{};let result={};if(params.action==='user_login_passwd')result={userId:20,objectId:'fixture',groupId:1,honeyName:'测试学员',sessionToken:'r:fixture',passwordResetRequired:true};if(params.action==='user_get')return r.fulfill({json:{retcode:0,result:{userId:20,groupId:1,passwordResetRequired:true},addon:{State:1}}});if(params.action==='app_password_change'){changes++;if(changes===1)return r.fulfill({status:503,json:{retmsg:'本地测试:保存暂时不可用'}});result={changed:true};}if(params.action==='app_deletion_status')result={items:[]};return r.fulfill({json:{retcode:0,result}});}return r.continue();});
- await p.goto('http://127.0.0.1:4202/pages/member/wxauth');await p.getByLabel('账号',{exact:true}).fill('fixture');await p.getByLabel('密码',{exact:true}).fill('p'.repeat(72));await p.locator('input[name=privacy]').check();await p.getByRole('button',{name:'登录',exact:true}).click();await p.waitForURL('**/pages/member/passwd');await p.getByLabel('当前密码',{exact:true}).fill('p'.repeat(72));await p.getByLabel('新密码',{exact:true}).fill('updated-password');await p.getByLabel('确认新密码',{exact:true}).fill('updated-password');await p.getByRole('button',{name:'保存并重新登录'}).click();await p.getByRole('alert').waitFor();assert.equal(await p.getByLabel('新密码',{exact:true}).inputValue(),'updated-password');await p.getByRole('button',{name:'保存并重新登录'}).click();await p.waitForURL('**/wxauth?passwordChanged=1');await p.getByText('密码已修改,请使用新密码重新登录').waitFor();assert.equal(changes,2);results.push({case:'72-character login and forced reset, failure retains input, success relogin',passed:true});
- const admin=await browser.newPage({viewport:{width:1366,height:900}});admin.on('pageerror',e=>errors.push(e.message));await admin.addInitScript(()=>localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify({objectId:'fixture',sessionToken:'r:fixture',username:'fixture',roles:['ops-manager'],operationsRole:'ops-manager',company:{objectId:'fixture'}})));
- let cutoff={month:'2026-10',version:'v1',runId:'fixture',cutoffAt:'2026-10-13T15:00:00Z',complete:true,pending:1,approved:false,monthClosed:false,datasets:[{model:54,source:1,matched:1}],conflicts:[{id:'f',dataset:'59',legacyKey:'1',student:'测试学员',course:'测试课程',date:'2026-10-12',fields:['teacherPaySnapshot'],current:{teacherPaySnapshot:20},fingerprint:'fp',cosmetic:false,resolved:false}]};let confirmations=0;
- await admin.route('**/*',async r=>{const u=new URL(r.request().url());if(!['127.0.0.1','localhost'].includes(u.hostname))return r.abort();if(u.pathname.startsWith('/cloud-functions')){const q=r.request().postDataJSON()?.params||{};let data={items:[],total:0,page:1,pageSize:20};if(q.operation==='meta')data={identity:{objectId:'fixture',roles:['ops-manager'],operationsRole:'ops-manager',isSuperAdmin:false,company:{objectId:'fixture'}},classes:0,cloudFunctions:1};if(q.operation==='ops/payroll/cutoff')data=cutoff;if(q.operation==='ops/payroll/cutoff-confirm'){confirmations++;assert.equal(q.payload.month,'2026-10');if(q.payload.conflictId){assert.equal(q.payload.fingerprint,'fp');cutoff={...cutoff,pending:0,version:'v2',conflicts:cutoff.conflicts.map(x=>({...x,resolved:true}))};}else cutoff={...cutoff,approved:true};data=cutoff;}if(q.operation==='ops/payroll/overview')data={month:'2026-10',coachItems:[],lines:[],stores:[],batches:[],coachCount:0,lessonCount:0,totalHours:0,baseAmount:0,reviewAmount:0,reviewCount:0,payableAmount:0,exceptionCount:0,sourceStatus:{mode:'parse',refreshedAt:'2026-10-13T15:00:00Z',sync:{state:'batch_verified',settlementReady:cutoff.approved,lastSuccessAt:'2026-10-13T15:00:00Z',lagSeconds:0}}};if(q.operation==='ops/teaching-rules/list')data={categories:[],rules:[],mappings:[],summary:{totalCourses:0,confirmed:0,pending:0,activeRules:0}};if(q.operation==='ops/vocabulary/tree')data={items:[],totalWords:0};if(q.operation==='ops/live/versions')data={version:'v',scopeVersions:{},syncStatus:{state:'current'}};return r.fulfill({json:{success:true,data}});}return r.continue();});
- await admin.goto('http://127.0.0.1:4302/admin/payroll');await admin.getByRole('button',{name:/核验 .*结算来源/}).click();await admin.getByText('待核对 1 条',{exact:false}).waitFor();await admin.getByLabel('核验依据').fill('核对旧记录后,保留当前工资快照');await admin.locator('app-payroll-cutoff summary').click();await admin.getByRole('button',{name:'确认此记录保留新系统'}).click();await admin.getByRole('button',{name:'确认该月份截止来源'}).click();await admin.getByText('来源已确认;数据变化后确认将自动失效。').waitFor();assert.equal(confirmations,2);results.push({case:'payroll cutoff explicit per-conflict confirmation and month approval',passed:true});await admin.evaluate(()=>window.scrollTo(0,0));await admin.screenshot({path:root+'/payroll-cutoff.png',fullPage:true});
- await admin.goto('http://127.0.0.1:4302/admin/special-training');const opener=admin.getByRole('button',{name:'新增专项',exact:true});await opener.click();await admin.getByRole('dialog',{name:'编辑专项训练'}).waitFor();await admin.waitForTimeout(100);assert(await admin.getByRole('dialog').evaluate(el=>el.contains(document.activeElement)));await admin.keyboard.press('Shift+Tab');assert(await admin.getByRole('dialog').evaluate(el=>el.contains(document.activeElement)));await admin.keyboard.press('Escape');await admin.getByRole('dialog').waitFor({state:'hidden'});assert(await opener.evaluate(el=>el===document.activeElement));results.push({case:'drawer initial focus, focus trap, Escape and focus return',passed:true});
- await admin.goto('http://127.0.0.1:4302/admin/vocabulary');await admin.locator('input[type=file]').first().setInputFiles({name:'oversized.xlsx',mimeType:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',buffer:Buffer.alloc(5*1024*1024+1)});await admin.getByRole('alert').filter({hasText:'5 MB'}).waitFor();const XLSX=require(process.cwd()+'/node_modules/xlsx');const book=XLSX.utils.book_new();XLSX.utils.book_append_sheet(book,XLSX.utils.aoa_to_sheet([['单词','释义'],['apple','苹果']]),'词库');await admin.locator('input[type=file]').first().setInputFiles({name:'fixture.xlsx',mimeType:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',buffer:XLSX.write(book,{type:'buffer',bookType:'xlsx',compression:true})});await admin.getByRole('dialog',{name:'导入词库'}).waitFor();results.push({case:'XLSX oversized rejection and valid compressed workbook preview',passed:true});
- const pdfPage=await browser.newPage();await pdfPage.setContent('<main id="report" style="width:700px;font:20px Arial">'+Array.from({length:65},(_,i)=>'<p>Learning report '+i+' — local export verification</p>').join('')+'</main>');await pdfPage.addScriptTag({path:process.cwd()+'/node_modules/html2pdf.js/dist/html2pdf.bundle.min.js'});const bytes=await pdfPage.evaluate(async()=>await html2pdf().set({margin:10,html2canvas:{scale:1},jsPDF:{unit:'mm',format:'a4',orientation:'portrait'}}).from(document.querySelector('#report')).toPdf().outputPdf('datauristring'));const pdf=Buffer.from(bytes.split(',')[1],'base64');fs.writeFileSync(root+'/export-fixture.pdf',pdf);const pages=(pdf.toString('latin1').match(/\/Type \/Page\b/g)||[]).length;assert(pages>=2);results.push({case:'html2pdf real multi-page PDF generation',pages,bytes:pdf.length,passed:true});
- for(const page of [p,admin]){assert.equal(await page.locator('#runtime-notice').count(),0);assert.deepEqual(await page.evaluate(()=>JSON.parse(sessionStorage.getItem('xiaoshu.safe-diagnostics.v1')||'[]').filter(x=>x.kind==='runtime')),[]);}assert.deepEqual(errors,[]);fs.writeFileSync(root+'/security-ui-results.json',JSON.stringify({results,pageErrors:errors},null,2));console.log(JSON.stringify(results));await browser.close();})().catch(e=>{console.error(e);process.exit(1)});
|