// No network or real rewards: exercise the panel's request/animation handoff. const fs = require('fs'), ts = require('typescript'), assert = require('node:assert/strict'); class Node { constructor(name) { this.name = name; this.children = []; this.active = true; this.position = {x: 200, y: 0}; } addChild(n) { this.children.push(n); } getChildByName(name) { return this.children.find(n => n.name === name); } setPosition(p) { this.position = p; } setContentSize() {} addComponent() { return {arc() {}, stroke() {}}; } destroy() { this.destroyed = true; } } const cc = { Component: class {}, Node, Graphics: class {}, Color: class {}, _decorator: {ccclass: () => C => C, property: (...args) => args.length >= 2 ? undefined : () => {}}, isValid: n => !n.destroyed, Tween: {stopAllByTarget() {}}, tween: () => ({by() {return this;}, repeatForever() {return this;}, start() {}}) }; const source = fs.readFileSync('assets/coin_madness/script/CoinMadnessPanel.ts', 'utf8'); const code = ts.transpileModule(source, {compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS, experimentalDecorators: true }, reportDiagnostics: true}); assert.equal(code.diagnostics.length, 0); const out = {}; new Function('require', 'exports', 'cc', code.outputText)(() => ({default: class {}}), out, cc); function fixture() { const panel = new out.default(), row = new Node('Stage0'); row.addChild(new Node('Claim')); panel.content = {children: [row]}; panel.message = {}; panel.data = {state: {claimed: [false]}}; const events = []; panel.render = data => { panel.data = data; events.push(panel.displayedClaims.has(0) ? 'claimed' : 'unclaimed'); row.getChildByName('Claim').active = !panel.displayedClaims.has(0) && !panel.claimWaiters.has(0); }; panel.showRewardAnimation = () => { assert(!panel.claimWaiters.has(0)); assert(panel.displayedClaims.has(0)); events.push('animation'); }; let resolve, reject, calls = 0; panel.api = {request: () => {calls++; return new Promise((yes, no) => {resolve = yes; reject = no;});}, toast() {}}; return {panel, row, events, resolve: value => resolve(value), reject: error => reject(error), calls: () => calls}; } (async () => { let f = fixture(); const pending = f.panel.run('claim', {stage: 0}); assert(f.panel.claimWaiters.has(0)); assert(!f.row.getChildByName('Claim').active); const spinner = f.panel.claimWaiters.get(0); await f.panel.run('claim', {stage: 0}); assert.equal(f.calls(), 1); f.panel.rewardSaved(0); assert(spinner.destroyed); assert.deepEqual(f.events, ['claimed', 'animation']); // A stale response after wallet persistence must not turn the button back on. f.resolve({code: 1, data: {state: {claimed: [false]}}}); await pending; assert(!f.row.getChildByName('Claim').active); await f.panel.run('claim', {stage: 0}); assert.equal(f.calls(), 1); for (const throws of [false, true]) { f = fixture(); const failed = f.panel.run('claim', {stage: 0}); const waiting = f.panel.claimWaiters.get(0); if (throws) f.reject(new Error('offline')); else f.resolve({code: 0, msg: 'retry'}); await failed; assert(waiting.destroyed); assert.equal(f.panel.claimWaiters.size, 0); assert(f.row.getChildByName('Claim').active); assert(!f.events.includes('animation')); } f = fixture(); const other = new Node('Stage1'); other.addChild(new Node('Claim')); f.panel.content.children.push(other); const queued = []; f.panel.api.request = (action, body) => new Promise(resolve => queued.push({stage: body.stage, resolve})); const first = f.panel.run('claim', {stage: 0}); f.panel.rewardSaved(0); // Coins are flying; confirmation is still pending. const second = f.panel.run('claim', {stage: 1}); assert.deepEqual(queued.map(q => q.stage), [0, 1]); assert(f.panel.claimWaiters.has(1)); assert(!other.getChildByName('Claim').active); await f.panel.run('claim', {stage: 1}); assert.equal(queued.length, 2); queued[0].resolve({code: 1}); await first; assert(f.panel.claimWaiters.has(1), 'first completion must not clear another row spinner'); queued[1].resolve({code: 1}); await second; assert.equal(f.panel.claimWaiters.size, 0); assert(source.includes('.to(.06, { scale: .75 }')); console.log('PASS: row spinner, duplicate suppression, synchronized claimed state/animation, stale response and failure recovery; coins start at 75%.'); })().catch(error => {console.error(error); process.exitCode = 1;});