|
@@ -0,0 +1,93 @@
|
|
|
|
|
+// 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;i<answers.length;i++){
|
|
|
|
|
+ const answer=answers[i],question=actual[i],source=allQuestions.find(q=>q.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,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();}
|