// Run the real H5 with isolated API responses; no business requests reach production. // READING_FIXTURE_DIR contains articles.json and questions.json (arrays, or {results}). import { chromium } from 'playwright'; import { createServer } from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import assert from 'node:assert/strict'; const fixtures=process.env.READING_FIXTURE_DIR; assert.ok(fixtures,'Set READING_FIXTURE_DIR to private, anonymized fixtures'); const read=name=>{const x=JSON.parse(fs.readFileSync(path.join(fixtures,name)));return x.results||x;}; const articles=read('articles.json'),allQuestions=read('questions.json'); const out=path.resolve(process.env.READING_QA_DIR||'/tmp/reading-print-qa');fs.mkdirSync(out,{recursive:true}); const build=path.resolve(process.env.READING_BUILD_DIR||'dist/xiaoshu-mobile/browser'); const server=createServer((req,res)=>{const file=path.join(build,new URL(req.url,'http://localhost').pathname);const target=fs.existsSync(file)&&fs.statSync(file).isFile()?file:path.join(build,'index.html');res.setHeader('Content-Type',({'.js':'application/javascript','.css':'text/css','.html':'text/html','.png':'image/png','.svg':'image/svg+xml','.woff2':'font/woff2'})[path.extname(target)]||'application/octet-stream');res.end(fs.readFileSync(target));}); await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); const origin=process.env.READING_BASE_URL||`http://127.0.0.1:${server.address().port}`; const browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_BIN||'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',args:['--no-proxy-server']}); const errors=[],unexpected=[],logs=new Map(),saved=[],report=[]; async function contextFor(extra={}){ const ctx=await browser.newContext({viewport:{width:1200,height:900},acceptDownloads:true,...extra}); await ctx.addInitScript(()=>localStorage.setItem('xiaoshu.angular.session',JSON.stringify({token:'r:isolated-reading-fixture',user:{userId:100,groupId:1,honeyName:'验收样本'},student:null}))); await ctx.route('**/*',async route=>{ const req=route.request(),url=new URL(req.url()); if(url.pathname.includes('/cloud-functions/')||url.pathname.includes('/api/functions/')){ const p=req.postDataJSON()?.params||{};let result=[]; if(p.action==='app_session_validate')result={valid:true}; else if(p.action==='app_live_versions')result={version:'1',scopeVersions:{},syncStatus:{state:'current'}}; else if(p.action==='reading/me')result={objectId:'fixture-user',nickname:'验收样本'}; else if(p.action==='reading/get')result=articles.find(a=>a.objectId===p.objectId); else if(p.action==='reading/query'){ if(p.className==='SurveyLog')result=logs.has(p.where.surveyItem.objectId)?[logs.get(p.where.surveyItem.objectId)]:[]; else if(p.where.parent)result=allQuestions.filter(q=>q.parent.objectId.replace(/^SurveyItem\$/,'')===p.where.parent.objectId); else result=articles.slice(0,1); } else if(p.action==='reading/create'&&p.className==='SurveyLog'){ result={...p.value,objectId:`fixture-log-${saved.length+1}`,createdAt:new Date().toISOString()};logs.set(p.value.surveyItem.objectId,result);saved.push(result); } else {unexpected.push(p.action||url.pathname);return route.fulfill({status:500,json:{retcode:-1,retmsg:'Unexpected isolated request'}});} return route.fulfill({json:{retcode:0,result}}); } if(url.origin!==new URL(origin).origin)return route.abort(); return route.continue(); });return ctx; } const exerciseSnapshot=page=>page.locator('.question-block').evaluateAll(nodes=>nodes.map(n=>({title:n.querySelector('header strong').textContent,options:[...n.querySelectorAll('.option')].map(o=>({label:o.querySelector('span').textContent,value:o.querySelector('b').textContent})),answer:n.querySelector('.answer-analysis>p')?.textContent}))); const pdfSnapshot=page=>page.locator('.question-item').evaluateAll(nodes=>nodes.map(n=>({title:n.querySelector('h3').textContent.replace(/^\d+\.\s*/,''),options:[...n.querySelectorAll('p')].map(p=>({label:p.querySelector('b').textContent.replace('.',''),value:[...p.childNodes].filter(n=>n.nodeType===3).map(n=>n.textContent).join('').trim()}))}))); async function assertPdf(page,article,exercise){ await page.locator('.question-item').last().waitFor(); const actual=await pdfSnapshot(page);assert.deepEqual(actual,exercise.map(({answer,...q})=>q)); const answers=await page.locator('.answer-item').evaluateAll(nodes=>nodes.map(n=>({title:n.querySelector('h3').textContent.replace(/^Question \d+:\s*/,''),correct:n.querySelector('.correct-answer p b').textContent.replace('.',''),options:[...n.querySelectorAll(':scope>div p')].map(p=>({label:p.querySelector('b').textContent.replace('.',''),value:[...p.childNodes].filter(n=>n.nodeType===3).map(n=>n.textContent).join('').trim()})),explanation:n.querySelector('footer')?.textContent}))); const positions=[]; for(let i=0;iq.parent.objectId.replace(/^SurveyItem\$/,'')===article.objectId&&q.title===answer.title); assert.equal(answer.title,question.title);assert.deepEqual(answer.options,question.options); assert.equal(question.options.find(o=>o.label===answer.correct).value,source.options.find(o=>o.check).value); if(exercise[i].answer!==undefined)assert.equal(answer.explanation,exercise[i].answer); positions.push(answer.correct); } assert.deepEqual(['A','B','C','D'].map(x=>positions.filter(p=>p===x).length).sort(),[1,1,1,2]); return positions.join(''); } try{ const context=await contextFor(),page=await context.newPage();page.on('pageerror',e=>errors.push(e.message)); for(const article of articles){ await page.goto(`${origin}/words/reading?sid=${article.objectId}&wordList=apple&name=验收样本`); await page.locator('.question-block').last().waitFor();const snapshot=await exerciseSnapshot(page); await page.getByRole('button',{name:'打印',exact:true}).click();await page.waitForURL('**/words/reading/pdf/**'); const answers=await assertPdf(page,article,snapshot);report.push({article:article.objectId,answers,questions:snapshot.length}); } // Full submission/retry/refresh/PDF-return path, with actual generated prose containing label references. const article=articles.find(a=>allQuestions.some(q=>q.parent.objectId.replace(/^SurveyItem\$/,'')===a.objectId&&/B项/.test(q.answer||'')))||articles[0]; await page.goto(`${origin}/words/reading?sid=${article.objectId}&wordList=apple`);await page.locator('.question-block').last().waitFor(); for(const block of await page.locator('.question-block').all()){ const title=await block.locator('header strong').textContent();const source=allQuestions.find(q=>q.parent.objectId.replace(/^SurveyItem\$/,'')===article.objectId&&q.title===title); const buttons=await block.locator('.option').all();for(const b of buttons)if(await b.locator('b').textContent()===source.options.find(o=>o.check).value){await b.click();break;} } await page.getByRole('button',{name:/提交并保存/}).click();await page.getByRole('button',{name:'查看解析',exact:true}).click(); assert.equal(saved.at(-1).grade,100);const completed=await exerciseSnapshot(page); await page.getByRole('button',{name:'打印',exact:true}).click();await assertPdf(page,article,completed); const downloadPromise=page.waitForEvent('download');await page.getByRole('button',{name:'导出为 PDF',exact:true}).click();const download=await downloadPromise;await download.saveAs(path.join(out,'verified-reading.pdf')); await page.locator('.pdf-document').screenshot({path:path.join(out,'print-preview.png')}); await page.getByRole('button',{name:'返回',exact:true}).click();await page.getByRole('button',{name:'再做一次',exact:true}).click();await page.waitForURL(/presentationVersion=v1-/); const retry=await exerciseSnapshot(page),retryUrl=page.url();assert.notDeepEqual(retry.map(({answer,...q})=>q),completed.map(({answer,...q})=>q)); await page.reload();await page.locator('.question-block').last().waitFor();assert.equal(await page.locator('.option.selected').count(),0);assert.deepEqual(await exerciseSnapshot(page),retry); await page.getByRole('button',{name:'打印',exact:true}).click();await assertPdf(page,article,retry); await page.getByRole('button',{name:'返回',exact:true}).click();await page.locator('.question-block').last().waitFor();assert.equal(await page.locator('.option.selected').count(),0);assert.deepEqual(await exerciseSnapshot(page),retry); await context.close(); const mobile=await contextFor({viewport:{width:390,height:844},isMobile:true,hasTouch:true,userAgent:'Mozilla/5.0 iPhone MicroMessenger/8.0'}),phone=await mobile.newPage();phone.on('pageerror',e=>errors.push(e.message)); await phone.goto(`${origin}/words/reading/pdf/${article.objectId}`);await phone.locator('.question-item').last().waitFor(); assert.equal(await phone.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false); await phone.getByRole('button',{name:'导出为 PDF',exact:true}).click();await phone.locator('.wechat-dialog').waitFor();await phone.screenshot({path:path.join(out,'wechat-preview.png')});await mobile.close(); assert.deepEqual(unexpected,[]);assert.deepEqual(errors,[]); fs.writeFileSync(path.join(out,'browser-summary.json'),JSON.stringify({origin,sets:report.length,questions:report.reduce((n,x)=>n+x.questions,0),report,isolatedSubmissions:saved.length,submittedGrade:saved[0].grade,retryRefreshAndPdfReturn:true,pdfExport:true,wechatViewport:true,physicalDevice:false,errors,unexpected},null,2)); console.log(JSON.stringify({sets:report.length,questions:report.length*5,isolatedSubmissions:saved.length,pdfExport:true,retryRefreshAndPdfReturn:true,errors})); }finally{await browser.close();server.close();}