const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const { loadMembers, withNewPlayerConfig } = require('./new-player-ab-test-support.cjs'); const imageNames = ['icon', 'Introduction', 'title']; const readTable = name => JSON.parse(fs.readFileSync(`assets/resources/Json/${name}.json`, 'utf8')).NEW_LEVEL; function setup(group = 'A') { const config = withNewPlayerConfig({ GM_INFO: { level: 6, abTestAssignments: { layer_1: { name: 'new1_50', group, enabled: true } }, } }); config.defaultNewLevel = readTable('NEW_LEVEL'); config.newPlayerNewLevel = readTable('NEW_LEVEL2'); const requests = [], errors = []; const sprites = Object.fromEntries(imageNames.map(name => [name, { spriteFrame: { stale: true } }])); const nodes = Object.fromEntries(imageNames.map(name => [name, { width: 200, height: 100, scale: 0.75, getComponent: () => sprites[name], }])); // Only the replacement image nodes exist; removed light/daoju children must not be required. const newMode = { getChildByName: name => nodes[name] || null }; const cc = { Sprite: class {}, SpriteFrame: class {}, fx: { GameConfig: config }, isValid: object => !!object && object.valid !== false, error: (...args) => errors.push(args), resources: { load(path, type, done) { assert.equal(type, cc.SpriteFrame); requests.push({ path, done }); } }, }; const Controller = loadMembers('assets/NewMode/script/NewMode.ts', [ 'propName', 'imageLoadVersion', 'onEnable', 'onDisable', ], { cc }); const panel = Object.assign(new Controller(), { node: { getChildByName: name => name === 'newmode' ? newMode : null }, }); return { config, requests, errors, sprites, nodes, panel }; } for (const group of ['A', 'B']) { test(`${group} uses each real unlock entry's image number, including 0, 33 and reversed 21/22`, () => { const { panel, config, requests, sprites } = setup(group); for (const entry of config.NEW_LEVEL) { config.GM_INFO.level = entry.level - 1; requests.length = 0; panel.onEnable(); const number = entry.name.slice('daoju'.length); assert.deepEqual(requests.map(request => request.path), imageNames.map(name => `Window_Prop/${name}${number}`)); assert.ok(Object.values(sprites).every(sprite => sprite.spriteFrame === null)); for (const request of requests) request.done(null, request.path); assert.deepEqual(Object.values(sprites).map(sprite => sprite.spriteFrame), imageNames.map(name => `Window_Prop/${name}${number}`)); } config.GM_INFO.level = group === 'B' ? 5 : 6; requests.length = 0; panel.onEnable(); assert.equal(requests[0].path, group === 'B' ? 'Window_Prop/icon2' : 'Window_Prop/icon0'); }); } test('reopening refreshes all images and ignores a previous opening that finishes later', () => { const { panel, config, requests, sprites } = setup(); panel.onEnable(); const first = requests.splice(0); first[0].done(null, 'old icon'); panel.onDisable(); config.GM_INFO.level = 14; panel.onEnable(); assert.ok(Object.values(sprites).every(sprite => sprite.spriteFrame === null)); for (const request of requests) request.done(null, request.path); for (const request of first) request.done(null, 'late old image'); assert.deepEqual(Object.values(sprites).map(sprite => sprite.spriteFrame), imageNames.map(name => `Window_Prop/${name}1`)); }); for (const [name, invalidate] of [ ['closed panel', ({ panel }) => panel.onDisable()], ['destroyed panel', ({ panel }) => { panel.valid = false; }], ['destroyed sprites', ({ sprites }) => Object.values(sprites).forEach(sprite => { sprite.valid = false; })], ]) { test(`pending images cannot be assigned to a ${name}`, () => { const context = setup(); context.panel.onEnable(); invalidate(context); for (const request of context.requests) request.done(null, 'late image'); assert.ok(Object.values(context.sprites).every(sprite => sprite.spriteFrame === null)); }); } test('one failed image stays clear while successful images are displayed', () => { const { panel, requests, errors, sprites } = setup(); panel.onEnable(); requests[0].done(new Error('image unavailable'), 'invalid image'); requests[1].done(null, 'introduction image'); requests[2].done(null, 'title image'); assert.equal(sprites.icon.spriteFrame, null); assert.equal(sprites.Introduction.spriteFrame, 'introduction image'); assert.equal(sprites.title.spriteFrame, 'title image'); assert.equal(errors.length, 1); }); test('unmatched and invalid config entries clear stale images without requesting assets', () => { for (const name of [null, 'daoju34', 'daoju-1', 'daoju1.5', 'other2', 'daoju2extra']) { const { panel, config, requests, sprites } = setup(); config.defaultNewLevel = name === null ? [] : [{ level: 7, name }]; panel.onEnable(); assert.equal(requests.length, 0, String(name)); assert.ok(Object.values(sprites).every(sprite => sprite.spriteFrame === null), String(name)); } }); test('missing image children are tolerated and existing nodes keep their layout', () => { const { panel, nodes, sprites, requests } = setup(); delete nodes.Introduction; delete sprites.title; panel.onEnable(); assert.deepEqual(requests.map(request => request.path), ['Window_Prop/icon0']); requests[0].done(null, 'icon image'); assert.deepEqual([nodes.icon.width, nodes.icon.height, nodes.icon.scale], [200, 100, 0.75]); });