119 lines
12 KiB
JavaScript
119 lines
12 KiB
JavaScript
'use strict';
|
||
|
||
const http=require('http');
|
||
const path=require('path');
|
||
const {spawn}=require('child_process');
|
||
const {chromium}=require('playwright-core');
|
||
const {assert,root}=require('./helpers/app-source');
|
||
|
||
const port=61000+Math.floor(Math.random()*1000);
|
||
const url=`http://127.0.0.1:${port}/`;
|
||
const edgePath=process.env.BEND_FIELD_BROWSER_PATH||process.env.BEND_FIELD_EDGE_PATH||'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
|
||
const sleep=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds));
|
||
const server=spawn(process.execPath,['server.js'],{cwd:root,env:{...process.env,HOST:'127.0.0.1',PORT:String(port)},stdio:'ignore'});
|
||
let browser=null;
|
||
|
||
async function waitForServer(){
|
||
for(let attempt=0;attempt<80;attempt++){
|
||
const ready=await new Promise(resolve=>{
|
||
const request=http.get(url,response=>{response.resume();resolve(response.statusCode===200)});
|
||
request.on('error',()=>resolve(false));request.setTimeout(250,()=>{request.destroy();resolve(false)});
|
||
});
|
||
if(ready)return;
|
||
await sleep(100);
|
||
}
|
||
throw new Error('Local game server did not start');
|
||
}
|
||
|
||
(async()=>{
|
||
try{
|
||
await waitForServer();
|
||
browser=await chromium.launch({executablePath:edgePath,headless:true});
|
||
const page=await browser.newPage({viewport:{width:1000,height:900}});
|
||
const browserErrors=[];page.on('pageerror',error=>browserErrors.push(String(error?.stack||error)));page.on('console',message=>{if(message.type()==='error')browserErrors.push(message.text())});
|
||
await page.goto(url,{waitUntil:'domcontentloaded'});
|
||
try{await page.waitForFunction(()=>document.body?.dataset?.ready==='true')}
|
||
catch(error){console.error('Store UI startup diagnostics:',JSON.stringify({errors:browserErrors,state:await page.evaluate(()=>({ready:document.body?.dataset?.ready||null,status:document.querySelector('#statusMessage')?.textContent||'',text:document.body?.innerText?.slice(0,500)||''}))}));throw error}
|
||
await page.evaluate(async()=>{await document.fonts.load('16px DotGothic16Local','日本語盤面');await document.fonts.ready});
|
||
const result=await page.evaluate(async()=>{
|
||
const meta=data.metas.B0,st=metaState('B0'),itemIds=seededStoreItemIds(meta.seed);
|
||
st.store={owner:'UI TEST',pathIndex:0,cellIndex:0,itemIds,purchases:[],priceVersion:STORE_PRICE_VERSION,priceCoefficient:1};
|
||
data.score=1e9;openStoreMeta(meta);
|
||
const itemSection=document.querySelector('.store-items-section'),cursorSection=document.querySelector('.store-cursors-section'),
|
||
cursorCards=[...cursorSection.querySelectorAll('.store-item')],itemList=itemSection.querySelector('.store-section-list'),
|
||
cursorList=cursorSection.querySelector('.store-section-list'),flag=FLAG_CURSOR_ITEMS.find(item=>item.id==='cursor-flag-jp');
|
||
const itemColumns=getComputedStyle(itemList).gridTemplateColumns.split(' ').length,cursorColumns=getComputedStyle(cursorList).gridTemplateColumns.split(' ').length,
|
||
cursorVertical=cursorCards.every(card=>card.querySelector('.store-item-icon').getBoundingClientRect().bottom<=card.querySelector('.store-buy').getBoundingClientRect().top+1);
|
||
const pricesBefore=[...document.querySelectorAll('.store-buy:not(:disabled)')].map(button=>button.textContent);
|
||
data.score=0;renderStorePanel();
|
||
const pricesAfter=[...document.querySelectorAll('.store-buy')].map(button=>button.textContent);
|
||
const storeResult={
|
||
headings:[...document.querySelectorAll('.store-section-title')].map(node=>node.textContent),
|
||
items:itemSection.querySelectorAll('.store-item').length,cursors:cursorCards.length,
|
||
cursorHasCopy:cursorCards.some(card=>card.querySelector('h4,p,strong,.store-item-copy')),
|
||
cursorIcons:cursorCards.map(card=>Boolean(card.querySelector('.store-item-icon')?.textContent||card.querySelector('.store-item-icon img')?.getAttribute('src'))),
|
||
itemColumns,cursorColumns,cursorVertical,
|
||
pricesBefore,pricesAfter,actualPrices:pricesAfter.every(text=>/[\d,]+$/.test(text)&&!text.includes('あと')&&!text.endsWith('個'))
|
||
};
|
||
st.store.purchases.push({id:flag.id,buyer:'UI TEST',boughtAt:1,paidCost:3000,usedAt:0},{id:'score-lens',buyer:'UI TEST',boughtAt:2,paidCost:200000,usedAt:0});inventoryCache=null;applyCursorStyle('default');
|
||
closeStore(false);renderInventoryPanel();
|
||
const inventoryOptions=[...document.querySelectorAll('.inventory-cursor-option')],inventoryHasCopy=Boolean(document.querySelector('.inventory-cursors-section p,.inventory-cursors-section strong,.inventory-cursors-section .store-item-copy'));
|
||
const persistentSave=save;save=async()=>true;
|
||
await useInventoryItemLoaded(flag.id);const selectedOnce=data.cursorStyle;
|
||
await useInventoryItemLoaded(flag.id);const selectedTwice=data.cursorStyle;
|
||
const lensButton=[...document.querySelectorAll('.inventory-item h3')].find(node=>node.textContent.startsWith('ジェムレンズ'))?.closest('.inventory-item')?.querySelector('.inventory-use'),
|
||
lensInitial=lensButton?.textContent;
|
||
await useInventoryItemLoaded('score-lens');const lensOn=data.scoreLensEnabled,lensOnText=[...document.querySelectorAll('.inventory-item h3')].find(node=>node.textContent.startsWith('ジェムレンズ'))?.closest('.inventory-item')?.querySelector('.inventory-use')?.textContent;
|
||
await useInventoryItemLoaded('score-lens');const lensOff=data.scoreLensEnabled,lensOffText=[...document.querySelectorAll('.inventory-item h3')].find(node=>node.textContent.startsWith('ジェムレンズ'))?.closest('.inventory-item')?.querySelector('.inventory-use')?.textContent;
|
||
save=persistentSave;
|
||
applyCursorStyle(flag.cursorStyle);
|
||
viewport.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,clientX:300,clientY:300,pointerType:'mouse'}));
|
||
await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
|
||
applyCursorStyle(flag.cursorStyle);
|
||
const board=rendered.get('B0'),dragCache=buildDragCache(board,0,false,1);updateDragCursorDesign(dragCache,[50,50]);
|
||
const dragDesign={image:dragCache.cursorImage.getAttribute('href'),display:getComputedStyle(dragCache.cursorImage).display,clip:dragCache.cursorImage.getAttribute('clip-path')};
|
||
clearDragRender(board);
|
||
const gemAnimationStarted=playGemCollectionAnimation(board,1000),gemParticles=document.querySelectorAll('.gem-particle').length;document.querySelectorAll('.gem-particle').forEach(node=>node.remove());
|
||
const normalDisplay=getComputedStyle(customEmojiCursor).display;document.body.classList.add('is-drawing');const drawingDisplay=getComputedStyle(customEmojiCursor).display;document.body.classList.remove('is-drawing');
|
||
const firstNumber=board.numberNodes.values().next().value,firstWarning=board.numberWarningNodes.values().next().value;
|
||
return{
|
||
flags:FLAG_CURSOR_ITEMS.length,fontReady:document.fonts.check('16px DotGothic16Local'),fontFaces:[...document.fonts].map(face=>({family:face.family,status:face.status,weight:face.weight})),
|
||
bodyFont:getComputedStyle(document.body).fontFamily,buttonFont:getComputedStyle(document.querySelector('button')).fontFamily,numberFont:getComputedStyle(firstNumber).fontFamily,
|
||
faceMin:Math.min(...YELLOW_FACE_CURSOR_ITEMS.map(item=>item.cost)),faceMax:Math.max(...YELLOW_FACE_CURSOR_ITEMS.map(item=>item.cost)),
|
||
flagBase:FLAG_CURSOR_ITEMS.find(item=>item.id==='cursor-flag-af').cost,oecdBase:flag.cost,shopChance:STORE_CHANCE,
|
||
meta:document.querySelector('#storeMeta').textContent,
|
||
...storeResult,inventoryOptions:inventoryOptions.length,inventoryHasCopy,selectedOnce,selectedTwice,lensInitial,lensOn,lensOnText,lensOff,lensOffText,
|
||
customText:customEmojiCursor.textContent,customImage:customEmojiCursor.querySelector('img')?.getAttribute('src')||'',customVisible:customEmojiCursor.classList.contains('visible'),
|
||
customOpacity:getComputedStyle(customEmojiCursor).opacity,customFont:getComputedStyle(customEmojiCursor).fontFamily,
|
||
customWidth:getComputedStyle(customEmojiCursor).width,customRadius:getComputedStyle(customEmojiCursor).borderRadius,customFlagFit:getComputedStyle(customEmojiCursor.querySelector('img')).objectFit,
|
||
normalDisplay,drawingDisplay,dragDesign,gemAnimationStarted,gemParticles,warningInFront:Boolean(firstNumber.compareDocumentPosition(firstWarning)&Node.DOCUMENT_POSITION_FOLLOWING),
|
||
gemLabel:document.querySelector('.stat.score').textContent.trim()
|
||
};
|
||
});
|
||
if(!result.fontReady)console.log('Store UI diagnostics:',JSON.stringify(result));
|
||
assert(result.flags===262,'Flag cursor catalog is incomplete');
|
||
assert(result.fontReady&&/DotGothic16Local/.test(result.bodyFont)&&/DotGothic16Local/.test(result.buttonFont)&&/DotGothic16Local/.test(result.numberFont),'Bundled Japanese dot font did not load or was overridden');
|
||
assert(result.faceMin===500&&result.faceMax===50000&&result.flagBase===10000&&result.oecdBase===20000&&Math.abs(result.shopChance-1/10)<1e-12,'Cursor prices or the 1/10 shop chance are incorrect');
|
||
assert(result.meta==='店主:UI TEST'&&!result.meta.includes('価格は固定'),'Fixed-price shop copy remains');
|
||
assert(result.headings.join('|')==='アイテム|カーソル','Shop sections are not separated');
|
||
assert(result.items===2&&result.cursors===12,'Shop does not render its 2+12 inventory');
|
||
assert(!result.cursorHasCopy&&result.cursorIcons.every(Boolean)&&result.actualPrices,'Cursor cards still expose names/descriptions, lack designs, or do not show actual prices');
|
||
assert(result.itemColumns===1&&result.cursorColumns===6&&result.cursorVertical,'Shop cursor designs are not horizontally arranged above their purchase buttons');
|
||
assert(result.inventoryOptions===1&&!result.inventoryHasCopy&&result.selectedOnce==='flag-jp'&&result.selectedTwice==='default',`Inventory cursor grid is not persistent or click-to-toggle: ${JSON.stringify({inventoryOptions:result.inventoryOptions,inventoryHasCopy:result.inventoryHasCopy,selectedOnce:result.selectedOnce,selectedTwice:result.selectedTwice})}`);
|
||
assert(result.lensInitial==='OFF'&&result.lensOn===true&&result.lensOnText==='ON'&&result.lensOff===false&&result.lensOffText==='OFF',`Score lens is not an ON/OFF inventory toggle: ${JSON.stringify({lensInitial:result.lensInitial,lensOn:result.lensOn,lensOnText:result.lensOnText,lensOff:result.lensOff,lensOffText:result.lensOffText})}`);
|
||
assert(!result.customText&&result.customImage.endsWith('assets/flags/1f1ef-1f1f5.svg')&&result.customVisible&&result.customOpacity==='0.76'&&result.customWidth==='14px'&&result.customRadius==='50%'&&result.customFlagFit==='cover','Japanese flag cursor is not a translucent circular knob-shaped SVG overlay');
|
||
assert(result.normalDisplay==='grid'&&result.drawingDisplay==='none'&&result.dragDesign.display!=='none'&&result.dragDesign.image.endsWith('assets/flags/1f1ef-1f1f5.svg')&&/^url\(#/.test(result.dragDesign.clip),`The custom cursor does not transfer to the grabbed knob: ${JSON.stringify({normalDisplay:result.normalDisplay,drawingDisplay:result.drawingDisplay,dragDesign:result.dragDesign})}`);
|
||
assert(result.gemAnimationStarted&&result.gemParticles>=4,'Purple gem collection particles did not spawn from the board');
|
||
assert(result.warningInFront&&!result.gemLabel.includes('ジェム')&&result.gemLabel.startsWith('◆'),'The 曲がる warning layer or standalone currency label is incorrect');
|
||
if(process.env.BEND_FIELD_UI_SCREENSHOT){
|
||
const base=path.resolve(process.env.BEND_FIELD_UI_SCREENSHOT);
|
||
await page.evaluate(()=>openStoreMeta(data.metas.B0));await page.screenshot({path:`${base}-store.png`});
|
||
await page.evaluate(()=>{closeStore(false);closeHelp(false,true);const flag=FLAG_CURSOR_ITEMS.find(item=>item.id==='cursor-flag-jp');applyCursorStyle(flag.cursorStyle);viewport.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,clientX:500,clientY:450,pointerType:'mouse'}))});
|
||
await page.screenshot({path:`${base}-cursor.png`});
|
||
}
|
||
console.log(`Store UI browser test passed: ${result.flags} flags, ${result.items}+${result.cursors} entries, ${result.cursorColumns} cursor columns`);
|
||
}finally{
|
||
if(browser)await browser.close();
|
||
server.kill();
|
||
}
|
||
})().catch(error=>{console.error(error);process.exitCode=1});
|