bend_puzzle/test/store-ui-browser-test.js
2026-08-01 16:06:14 +09:00

120 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 itemSections=[...document.querySelectorAll('.store-items-section')],cursorSection=document.querySelector('.store-cursors-section'),
cursorCards=[...cursorSection.querySelectorAll('.store-item')],itemCards=[...document.querySelectorAll('.store-other')],
itemList=itemSections.map(section=>section.querySelector('.store-section-list')).find(list=>list.querySelector('.store-item')),
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:itemCards.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.includes('カーソル')&&result.headings.includes('その他のアイテム')&&result.headings.length===2,'Shop cosmetic, tool, and cursor sections are not separated');
assert(result.items===12&&result.cursors===6,'Shop does not render its 12+6 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===6&&result.cursorColumns===6&&result.cursorVertical,'Shop cosmetics and cursor designs are not compact horizontal grids 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});