2026-07-30 15:13:53 +09:00
'use strict' ;
const http = require ( 'http' ) ;
const path = require ( 'path' ) ;
const { spawn } = require ( 'child_process' ) ;
2026-07-31 12:54:46 +09:00
const { chromium } = require ( 'playwright-core' ) ;
2026-07-30 15:13:53 +09:00
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 ) ;
2026-08-01 16:06:14 +09:00
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' ) ) ,
2026-07-30 15:13:53 +09:00
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 ) ,
2026-08-01 16:06:14 +09:00
items : itemCards . length , cursors : cursorCards . length ,
2026-07-30 15:13:53 +09:00
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' ) ;
2026-07-31 12:54:46 +09:00
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' ) ;
2026-07-30 15:13:53 +09:00
assert ( result . meta === '店主: UI TEST' && ! result . meta . includes ( '価格は固定' ) , 'Fixed-price shop copy remains' ) ;
2026-08-01 16:06:14 +09:00
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' ) ;
2026-07-30 15:13:53 +09:00
assert ( ! result . cursorHasCopy && result . cursorIcons . every ( Boolean ) && result . actualPrices , 'Cursor cards still expose names/descriptions, lack designs, or do not show actual prices' ) ;
2026-08-01 16:06:14 +09:00
assert ( result . itemColumns === 6 && result . cursorColumns === 6 && result . cursorVertical , 'Shop cosmetics and cursor designs are not compact horizontal grids above their purchase buttons' ) ;
2026-07-30 15:13:53 +09:00
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 } ) ;