const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); const assert = require('node:assert/strict'); const fs = require('node:fs'); (async () => { const browser = await chromium.launch({ headless: true, channel: 'msedge' }); try { const page = await browser.newPage({ viewport: { width: 450, height: 800 } }); const errors = []; page.on('pageerror', error => errors.push(error.message)); await page.goto('http://127.0.0.1:' + (process.env.PORT || 8765)); await page.waitForFunction(() => window.demo && demo.ready); await page.evaluate(() => demo.open()); await page.evaluate(async () => { const bundle = cc.assetManager.getBundle('coin_madness'); const prefab = await new Promise(resolve => bundle.load('prefab/CoinMadness', cc.Prefab, (error, asset) => resolve(asset))); const node = cc.instantiate(prefab); cc.director.getScene().children[0].addChild(node); node.zIndex = cc.macro.MAX_ZINDEX; window.apiPanel = node.getComponent('CoinMadnessPanel'); window.apiCalls = []; window.toasts = []; window.apiData = { serverNow: Date.now(), status: 'purchasable', state: { config: { periodId: 'goldMiner:2026-09-17', startsAt: Date.now() - 1000, endsAt: Date.now() + 3600000, priceCents: 600, stages: [0, 1, 2].map(i => ({ taskId: 'task-' + i, wins: (i + 1) * 2, coins: (i + 1) * 100, claimStatus: 'locked' })) }, paidAt: 0, totalWins: 6, claimed: [false, false, false], stage: 0, settled: false } }; apiPanel.initialize(null, { request: async (action, body) => { apiCalls.push({ action, body }); if (action === 'claim') { apiData.state.claimed[body.stage] = true; apiData.state.config.stages[body.stage].claimStatus = 'claimed'; apiData.state.config.stages[body.stage + 1].claimStatus = 'claimable'; } return { code: 1, data: JSON.parse(JSON.stringify(apiData)) }; }, close() { node.destroy(); }, openHelp() {}, toast: message => toasts.push(message), openSettlement() {} }); }); await page.waitForFunction(() => apiPanel.data && !apiPanel.busy); assert.equal(await page.evaluate(() => apiPanel.debugTools.active), false); assert.equal(await page.evaluate(() => apiPanel.priceDigits.text), '6.00'); assert.equal(await page.evaluate(() => apiPanel.purchaseButton.active), true); assert.equal(await page.evaluate(() => apiPanel.content.children[0].getChildByName('Claim').active), false); await page.evaluate(() => { apiData.status = 'unlocked'; apiData.state.paidAt = 1; apiData.state.config.stages[0].claimStatus = 'claimable'; apiPanel.render(apiData); apiPanel.content.children[1].emit(cc.Node.EventType.TOUCH_END, { stopPropagation() {} }); }); assert.equal(await page.evaluate(() => apiCalls.filter(c => c.action === 'claim').length), 0); assert.match(await page.evaluate(() => toasts[0]), /前一档/); await page.evaluate(() => { for (let i = 0; i < 2; i++) apiPanel.content.children[0].emit(cc.Node.EventType.TOUCH_END, { stopPropagation() {} }); }); await page.waitForFunction(() => !apiPanel.busy && apiPanel.data.state.claimed[0]); assert.equal(await page.evaluate(() => apiCalls.filter(c => c.action === 'claim').length), 1); assert.equal(await page.evaluate(() => apiPanel.purchaseButton.active), false); assert.equal(await page.evaluate(() => apiPanel.content.children[1].getChildByName('Claim').active), true); fs.mkdirSync('.codex_tmp/coinmadness-api', { recursive: true }); await page.screenshot({ path: '.codex_tmp/coinmadness-api/unlocked.png' }); await page.evaluate(() => { apiData.status = 'unavailable'; apiData.state.paidAt = 0; apiData.state.config.stages = []; apiData.state.config.endsAt = Date.now() - 1; delete apiData.state.config.priceCents; apiPanel.render(apiData); }); assert.equal(await page.evaluate(() => apiPanel.purchaseButton.active), false); assert.equal(await page.evaluate(() => apiPanel.content.children.filter(c => c.active).length), 0); assert.deepEqual(errors, []); console.log('PASS: real asynchronous panel path, dynamic price, hidden debug tools, sequential claim gating, double tap and unavailable state.'); } finally { await browser.close(); } })().catch(error => { console.error(error); process.exitCode = 1; });