// Usage (project root): node assets/test/tests/guide-carousel.cjs const fs = require('fs'); const vm = require('vm'); const path = require('path'); const assert = require('assert'); const ts = process.argv[2] ? require(path.resolve(process.argv[2])) : require("./preview.cjs").loadTypeScript(); class Vec { constructor(x = 0, y = 0) { this.x = x; this.y = y; } clone() { return new Vec(this.x, this.y); } } class Node { constructor(name) { this.name = name; this.children = []; this.position = new Vec(); this.scale = 1; this.active = true; this.components = new Map(); this.events = {}; } set parent(node) { this._parent = node; if (node) node.children.push(this); } get parent() { return this._parent; } set x(x) { this.position.x = x; } get x() { return this.position.x; } set y(y) { this.position.y = y; } get y() { return this.position.y; } setPosition(x, y) { this.position = new Vec(x, y); } setContentSize(w, h) { this.width = w; this.height = h; } addComponent(Type) { const component = new Type(); component.node = this; this.components.set(Type, component); return component; } getComponent(Type) { return typeof Type === "string" ? Array.from(this.components.values()).find(c => c.constructor.name === Type) : this.components.get(Type); } getChildByName(name) { return this.children.find(node => node.name === name); } on(event, handler, target) { this.events[event] = handler.bind(target); this.events[event].target = target; } emit(event, arg) { if (this.events[event]) this.events[event](arg); } removeAllChildren() { this.children = []; } removeFromParent() { if (this.parent) this.parent.children = this.parent.children.filter(n => n !== this); this._parent = null; } targetOff(target) { Object.keys(this.events).forEach(e => { if (this.events[e].target === target) delete this.events[e]; }); } destroy() {} convertToNodeSpaceAR(pos) { return pos.clone(); } } Node.EventType = Object.fromEntries(['TOUCH_START','TOUCH_MOVE','TOUCH_END','TOUCH_CANCEL','MOUSE_DOWN','MOUSE_MOVE','MOUSE_UP','MOUSE_LEAVE'].map(x => [x, x])); class Mask {} Mask.Type = { RECT: 0 }; class Graphics { rect() {} circle() {} fill() {} stroke() {} roundRect() {} } class Sprite {} Sprite.Type = { SLICED: 1 }; Sprite.SizeMode = { CUSTOM: 0 }; class Label {} Label.HorizontalAlign = { CENTER: 0 }; class Color {} Color.WHITE = {}; const config = JSON.parse(fs.readFileSync('assets/resources/Json/NEW_LEVEL.json', 'utf8')).NEW_LEVEL; let now = 1000; let deferImages = false; const pendingImages = []; const imageLoads = []; const frames = []; const cc = { Node, Mask, Graphics, Sprite, Label, Color, Button: class {}, BlockInputEvents: class {}, SpriteFrame: class {}, Component: class {}, _decorator: { ccclass: Type => Type, property: (...args) => args.length > 1 ? undefined : () => {} }, fx: { GameConfig: { NEW_LEVEL: config, GM_INFO: { level: 510 } } }, isValid: () => true, assetManager: { loadBundle(_name, cb) { cb(null, { load(_path, _type, callback) { const frame = { refs: 0, texture: {}, getTexture() { return this.texture; }, addRef() { this.refs++; return this; }, decRef() { this.refs--; return this; }, getOriginalSize: () => ({width:400,height:200}) }; frames.push(frame); imageLoads.push(_path); if (deferImages && !_path.endsWith('/mianba')) pendingImages.push(() => callback(null, frame)); else callback(null, frame); } }); } }, instantiate(asset) { return instantiatePrefab(asset); } }; cc.assetManager.loadBundle("resources", (_error, bundle) => cc.resources = bundle); let deferTweens = false; const tweens = []; cc.tween = target => { const tween = { stopped: false, steps: [], to(duration, props, options) { this.steps.push(() => Object.assign(target, props)); return this; }, call(callback) { this.steps.push(callback); return this; }, stop() { this.stopped = true; }, finish() { for (const step of this.steps) { if (this.stopped) break; step(); } }, start() { tweens.push(this); if (!deferTweens) this.finish(); return this; } }; return tween; }; const modules = {}; for (const name of ['LevelGameplay', 'GuideCarousel', 'GameplayIntro']) { const source = fs.readFileSync(name === 'LevelGameplay' ? 'assets/Script/module/Tool/LevelGameplay.ts' : 'assets/GameplayIntro/script/' + name + '.ts', 'utf8'); const result = ts.transpileModule(source, { compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } }); const exports = {}; vm.runInNewContext(result.outputText, { cc, exports, require: request => modules[request.slice(2)], Date: { now: () => now } }); modules[name] = exports; } function entriesForLevel(level) { const data = JSON.parse(fs.readFileSync('assets/custom/Json/level' + level + '.json', 'utf8')); return modules.LevelGameplay.getLevelGameplay(config, data); } function instantiatePrefab(data) { const objects = data.map(o => o.__type__ === 'cc.Node' ? new Node(o._name) : null); data.forEach((o, i) => { const node = objects[i]; if (!node) return; node.setContentSize(o._contentSize.width, o._contentSize.height); node.setPosition(o._trs.array[0], o._trs.array[1]); node.scale = o._trs.array[7]; if (o._parent) node.parent = objects[o._parent.__id__]; o._components.forEach(ref => { const c = data[ref.__id__]; const Type = c.__type__.startsWith('cc.') ? cc[c.__type__.slice(3)] : c.__type__.startsWith('b6043') ? modules.GameplayIntro.default : modules.GuideCarousel.default; const instance = node.addComponent(Type); if (c.dotFrame) instance.dotFrame = c.dotFrame.__uuid__; if (c.selectedDotFrame) instance.selectedDotFrame = c.selectedDotFrame.__uuid__; }); }); return objects[data[0].data.__id__]; } function setup(entries) { const popup = instantiatePrefab(JSON.parse(fs.readFileSync('assets/GameplayIntro/prefab/GameplayIntro.prefab', 'utf8'))); const component = popup.getComponent(modules.GameplayIntro.default); component.open(typeof entries === 'number' ? entriesForLevel(entries) : entries); return { component, carousel: component.carousel, popup }; } function event(x, id = 1, button = 0) { return { getID: () => id, getButton: () => button, getLocation: () => new Vec(x), stopPropagation() {} }; } function drag(carousel, delta, touch = false) { carousel[touch ? 'touchStart' : 'mouseDown'](event(0)); now += 250; carousel[touch ? 'touchMove' : 'mouseMove'](event(delta)); now += 150; carousel[touch ? 'touchEnd' : 'mouseUp'](event(delta)); carousel.update(0.4); } const names = entries => Array.from(entries, entry => entry.name); assert.deepEqual(names(entriesForLevel(1)), []); assert.deepEqual(names(entriesForLevel(7)), ['daoju0']); assert.deepEqual(names(entriesForLevel(15)), ['daoju1']); assert.deepEqual(names(entriesForLevel(44)), ['daoju3', 'daoju4']); assert.deepEqual(names(entriesForLevel(511)), ['daoju0', 'daoju2', 'daoju5', 'daoju11', 'daoju13']); // These two introduction assets describe different mechanics despite their unlock-table order. assert(names(entriesForLevel(1354)).includes('daoju27')); // extending rod assert(!names(entriesForLevel(1354)).includes('daoju26')); assert(names(entriesForLevel(1398)).includes('daoju26')); // moving colored gate assert(!names(entriesForLevel(1398)).includes('daoju27')); assert(names(entriesForLevel(1881)).includes('daoju32')); assert(!names(entriesForLevel(1923)).includes('daoju33')); // this local fixture has no spawn gates const filter = modules.LevelGameplay.getLevelGameplay; const data = (blocks = [], walls = [], level = {}) => ({ BLOCK_INFO: [blocks], WALL_INFO: [walls], LEVEL_INFO: [level] }); const block = fields => ({ block: 0, color: 1, type: 0, ...fields }); // Distinct variants must not add an unrelated basic explanation for the same object. const cases = [ [block({ type: 7 }), 'daoju0'], [block({ type: 1, stacking: 2 }), 'daoju1'], [block({ type: 4 }), 'daoju2'], [block({ type: 5 }), 'daoju3'], [block({ type: 2 }), 'daoju4'], [block({ type: 9 }), 'daoju5'], [block({ type: 6 }), 'daoju7'], [block({ color: 11 }), 'daoju11'], [block({ floor: 1, floorTime: 3 }), 'daoju12'], [block({ type: 14 }), 'daoju14'], [block({ type: 16 }), 'daoju16'], [block({ type: 9, lockTime: 3 }), 'daoju17'], [block({ type: 17 }), 'daoju18'], [block({ lock: false }), 'daoju19'], [block({ floor: 1, floorTime: 3, floorMove: true }), 'daoju20'], [block({ type: 1, colorChange: true }), 'daoju21'], [block({ type: 21 }), 'daoju22'], [block({ flowerReceive: 33, floor: 300, floorTime: 3000, floorMove: true }), 'daoju24'], [block({ colorArray: '12' }), 'daoju25'], [block({ type: 22 }), 'daoju28'], [block({ floor: 1, floorTime: 3, floorMove: false, floorColor: 2 }), 'daoju29'], [block({ type: 8, lockTime: 3 }), 'daoju31'] ]; for (const [definition, expected] of cases) { assert.deepEqual(names(filter(config, data([definition]))), [expected], JSON.stringify(definition)); } for (const [definition, expected] of [ [{ special: 2 }, 'daoju6'], [{ special: 3 }, 'daoju9'], [{ special: 4 }, 'daoju10'], [{ colorArray: '12' }, 'daoju13'], [{ special: 6 }, 'daoju14'], [{ longAndShort: 12 }, 'daoju15'], [{ special: 7 }, 'daoju23'], [{ special: 10 }, 'daoju26'], [{ special: 9 }, 'daoju27'], [{ toggle: 2 }, 'daoju30'] ]) assert.deepEqual(names(filter(config, data([], [definition]))), [expected]); assert.deepEqual(names(filter(config, data([], [], { risefall: [{ color: 2 }] }))), ['daoju8']); assert.deepEqual(names(filter(config, data([], [], { risefall: [{ unLockFall: true }] }))), ['daoju30']); assert.deepEqual(names(filter(config, data([], [], { risefall: [{ vine: 0 }] }))), []); assert.deepEqual(names(filter(config, data([], [], { risefall: [{ vine: 1 }] }))), ['daoju32']); const queued = data([block({ type: 4 })], [], { spawnGates: [{ spawnQueue: [{ members: [block({ type: 4 }), block({ type: 21 })] }] }] }); const before = JSON.stringify(queued); assert.deepEqual(names(filter(config, queued)), ['daoju2', 'daoju22', 'daoju33']); assert.equal(JSON.stringify(queued), before); assert.deepEqual(names(filter(config.concat(config), queued)), ['daoju2', 'daoju22', 'daoju33']); console.log('PASS: actual level filtering, variants, walls/tiles, generated blocks, deduplication and unmodified input'); let { component, carousel } = setup(511); assert.equal(component.getPreviewState().total, 5); assert.equal(component.getPreviewState().index, 0); assert.equal(component.getPreviewState().entry.name, 'daoju13'); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); assert.equal(carousel.offset, 0); assert.deepEqual(names(component.introductions), ['daoju13', 'daoju11', 'daoju5', 'daoju2', 'daoju0']); drag(carousel, -300); assert.equal(carousel.index, 1); assert.equal(component.getPreviewState().entry.name, 'daoju11'); assert.equal(carousel.dots[1].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); drag(carousel, 300, true); assert.equal(carousel.index, 0); carousel.goTo(4); carousel.update(0.4); carousel.mouseDown(event(0)); now += 100; carousel.mouseMove(event(-500)); assert(carousel.offset < -4 * 850 && carousel.offset > -4 * 850 - 120); carousel.mouseUp(event(-500)); carousel.update(0.1); const caught = carousel.offset; carousel.mouseDown(event(0)); carousel.mouseMove(event(0)); assert(Math.abs(carousel.offset - caught) < 0.001); carousel.mouseLeave(); carousel.update(0.4); assert.equal(carousel.offset, -4 * 850); component.previewNavigate('first'); carousel.update(0.4); assert.equal(carousel.index, 0); carousel.touchStart(event(0)); now += 100; carousel.touchMove(event(500)); assert(carousel.offset > 0 && carousel.offset < 120); const offset = carousel.offset; carousel.touchMove(event(900, 2)); assert.equal(carousel.offset, offset); carousel.touchCancel(event(500)); carousel.update(0.4); assert.equal(carousel.offset, 0); ({ component, carousel } = setup(7)); assert.equal(carousel.pages.length, 1); assert.equal(carousel.dots.length, 0); assert.equal(component.board.getChildByName('Pagination').active, false); assert.equal(Object.keys(carousel.node.events).filter(e => e !== 'page-changed').length, 0); carousel.mouseDown(event(0)); carousel.mouseMove(event(400)); assert.equal(carousel.offset, 0); carousel.touchStart(event(0)); carousel.touchMove(event(-400)); assert.equal(carousel.offset, 0); ({ component, carousel } = setup(44)); assert.equal(carousel.pages.length, 2); assert.equal(carousel.dots.length, 2); assert.equal(carousel.incomingDot, null); assert.equal(carousel.index, 0); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); assert.deepEqual(Array.from(carousel.dots, dot => dot.x), [-20, 20]); drag(carousel, -400); assert.equal(carousel.index, 1); assert.equal(carousel.dots[1].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); drag(carousel, -400); assert.equal(carousel.index, 1); drag(carousel, 400); assert.equal(carousel.index, 0); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); component.close(); component.open(entriesForLevel(511)); assert.equal(component.getPreviewState().index, 0); assert.equal(component.carousel.dots[0].getComponent(Sprite).spriteFrame, component.carousel.selectedDotFrame); assert.equal(component.board.getChildByName('Pagination').active, true); assert.equal(component.carousel.node.children.length, 1); component.open(entriesForLevel(7)); assert.equal(component.carousel.dots.length, 0); assert.equal(component.board.getChildByName('Pagination').children.length, 0); let closed = false; component.node.on('intro-closed', () => closed = true); const emptyLoadStart = imageLoads.length; component.open([]); assert(closed); assert.equal(component.node.active, false); assert.equal(component.getPreviewState().total, 0); assert.equal(imageLoads.length, emptyLoadStart); component.previewNavigate('next'); ({ component, carousel } = setup(1)); assert.equal(component.node.active, false); const source = fs.readFileSync('assets/GameplayIntro/script/GameplayIntro.ts', 'utf8'); assert(!source.includes('"Texture/"')); for (let id = 0; id < 34; id++) { const before = imageLoads.length; const { component: panel } = setup([{name: 'daoju' + id, level: id}]); const artId = id === 26 ? 27 : id === 27 ? 26 : id; assert.deepEqual(imageLoads.slice(before), ['title', 'icon', 'Introduction'].map(prefix => 'Window_Prop/' + prefix + artId)); for (const resource of imageLoads.slice(before)) assert(fs.existsSync('assets/resources/' + resource + '.png')); panel.board.getChildByName('CloseButton').emit('click'); assert.equal(panel.node.active, false); } // Image assignment must not override editor-authored dimensions or scale. for (const id of [0, 21]) { const { component: panel } = setup([{ name: 'daoju' + id, level: id }]); const description = panel.loadedPages[0].node.getChildByName('DescriptionImage'); const authored = JSON.parse(fs.readFileSync('assets/GameplayIntro/prefab/GameplayIntro.prefab', 'utf8')) .find(n => n._name === 'DescriptionImage'); assert.equal(description.y, authored._trs.array[1]); assert.equal(description.scale, authored._trs.array[7]); assert.equal(description.width, authored._contentSize.width); assert.equal(panel.loadedPages[0].node.children.length, 3); panel.close(); } const layoutPanel = setup([{ name: 'daoju0', level: 1 }]).component; layoutPanel.close(); const descriptionData = layoutPanel.carousel.pages[0].getChildByName('DescriptionImage'); descriptionData.setContentSize(500, 100); descriptionData.scale = 0.8; descriptionData.y = -280; layoutPanel.open([{ name: 'daoju0', level: 1 }]); assert.equal(layoutPanel.loadedPages[0].node.getChildByName('DescriptionImage').y, -280); assert.equal(layoutPanel.loadedPages[0].node.getChildByName('DescriptionImage').scale, 0.8); assert.equal(descriptionData.width, 500); assert.equal(descriptionData.height, 100); layoutPanel.close(); console.log('PASS: serialized page layout, editor size/position changes and no per-image correction'); // The viewport hierarchy is authored once and survives every page switch and reopen. const fixedPanel = setup(config).component; const viewport = fixedPanel.board.getChildByName('IntroViewport'); const collectNodes = node => [node, ...node.children.flatMap(collectNodes)]; const fixedNodes = collectNodes(viewport); const track = viewport.getChildByName('Pages'); assert.equal(track.children.length, 3); for (let i = 0; i < 34; i++) { fixedPanel.carousel.goTo(i, false); assert.deepEqual(collectNodes(viewport), fixedNodes); assert(Object.keys(fixedPanel.loadedPages).length <= 3); assert.equal(fixedPanel.carousel.pages[i].active, true); } fixedPanel.close(); assert.deepEqual(collectNodes(viewport), fixedNodes); fixedPanel.open(entriesForLevel(511)); assert.deepEqual(collectNodes(viewport), fixedNodes); fixedPanel.close(); console.log('PASS: fixed viewport hierarchy across all 34 pages, close and reopen'); // Sorting uses unlock levels, not the input order or the numerical artwork IDs. const shuffled = [config.find(e => e.name === 'daoju21'), config.find(e => e.name === 'daoju22')]; ({ component, carousel } = setup(shuffled)); assert.equal(component.getPreviewState().entry.name, 'daoju21'); assert.equal(carousel.index, 0); assert.deepEqual(names(shuffled), ['daoju21', 'daoju22']); component.close(); console.log('PASS: latest unlocked page at left edge, zero/single/two/many pages, reopen/reset, boundaries and 34 asset mappings'); // Window residency and actual texture references shrink on navigation/close. const loadStart = imageLoads.length; ({ component, carousel } = setup(config)); // Stress the renderer with every existing artwork. assert.equal(Object.keys(component.loadedPages).length, 2); assert.equal(imageLoads.length - loadStart, 6); for (let i = 0; i < 12; i++) { drag(carousel, -300); assert(Object.keys(component.loadedPages).length <= 3); assert(Object.keys(component.loadedPages).every(key => Math.abs(Number(key) - carousel.index) <= 1)); } const owned = Object.values(component.loadedPages).flatMap(page => page.frames); assert.equal(owned.length, 9); component.node.getChildByName('Backdrop').emit('click'); assert.equal(component.node.active, false); assert(owned.every(frame => frame.refs === 0)); deferImages = true; ({ component, carousel } = setup(config)); assert.equal(pendingImages.length, 1); component.previewNavigate('last'); component.close(); const loadsAtClose = imageLoads.length; pendingImages.shift()(); assert.equal(imageLoads.length, loadsAtClose); assert.equal(pendingImages.length, 0); assert.equal(Object.keys(component.loadedPages).length, 0); assert.equal(frames[frames.length - 1].refs, 0); deferImages = false; console.log('PASS: outside close, <=3 resident pages, sequential loads, released references and stale in-flight completion'); ({ component, carousel } = setup(511)); carousel.goTo(0, false); carousel.goTo(1); carousel.update(0.4); carousel.goTo(2); carousel.update(0.4); carousel.goTo(3); assert.equal(carousel.activeDot, 2); assert.equal(carousel.incomingDot.active, true); carousel.update(0.1); assert(carousel.dots[2].x < 40); assert(carousel.incomingDot.opacity > 0); carousel.update(0.3); assert.equal(carousel.incomingDot.active, false); carousel.goTo(2); carousel.update(0.4); carousel.goTo(1); carousel.update(0.4); carousel.goTo(0); assert.equal(carousel.dotDirection, 1); carousel.onDisable(); assert.equal(carousel.incomingDot.active, false); assert.equal(carousel.dots[0].getComponent(Sprite).spriteFrame, carousel.selectedDotFrame); carousel.goTo(-1); assert.equal(carousel.incomingDot.active, false); component.close(); console.log('PASS: edge dot replacement, symmetric motion, disable cleanup and boundary snap'); // Closing retains content and pause until the exit tween completes; reopening cancels it. deferTweens = true; ({ component } = setup(511)); assert.equal(component.board.scale, 0.3); tweens[tweens.length - 1].finish(); assert.equal(component.board.scale, 1); let closeEvents = 0; component.node.on('intro-closed', () => closeEvents++); component.close(); const exitTween = tweens[tweens.length - 1]; component.close(); assert.equal(tweens[tweens.length - 1], exitTween); assert.equal(closeEvents, 0); assert(component.node.active); assert(Object.keys(component.loadedPages).length > 0); component.open(entriesForLevel(511)); exitTween.finish(); assert(component.node.active); assert.equal(closeEvents, 0); component.close(); tweens[tweens.length - 1].finish(); assert.equal(component.node.active, false); assert.equal(closeEvents, 1); assert.equal(Object.keys(component.loadedPages).length, 0); deferTweens = false; console.log('PASS: animated close timing, duplicate close guard, reopen cancellation and resource cleanup'); // Scene teardown can destroy child nodes before the panel controller. ({ component } = setup(511)); component.close(); Object.defineProperty(component.board, 'scale', { set() { throw new Error('scale accessed after child destruction'); } }); assert.doesNotThrow(() => component.onDestroy()); ({ component } = setup(511)); const invalid = new Set(); for (const page of Object.values(component.loadedPages)) { invalid.add(page.node); Object.defineProperty(page.node, 'active', { set() { throw new Error('destroyed page accessed'); } }); for (const sprite of page.sprites) { invalid.add(sprite); Object.defineProperty(sprite, 'spriteFrame', { set() { throw new Error('destroyed sprite accessed'); } }); } } const previousIsValid = cc.isValid; cc.isValid = object => !invalid.has(object); const teardownFrames = Object.values(component.loadedPages).flatMap(page => page.frames); assert.doesNotThrow(() => component.onDestroy()); assert(teardownFrames.every(frame => frame.refs === 0)); assert.equal(Object.keys(component.loadedPages).length, 0); cc.isValid = previousIsValid; console.log('PASS: teardown after child destruction without node writes or leaked image references'); // Exercise the real question-entry controller against the serialized game scene. const sceneData = JSON.parse(fs.readFileSync('assets/Scene/GameScene.fire', 'utf8')); const sceneNodes = sceneData.map(o => o.__type__ === 'cc.Node' ? new Node(o._name) : null); sceneData.forEach((o, i) => { if (sceneNodes[i] && o._parent && sceneNodes[o._parent.__id__]) sceneNodes[i].parent = sceneNodes[o._parent.__id__]; }); const canvas = sceneNodes.find(n => n && n.name === 'Canvas'); const infoButton = sceneNodes.find(n => n && n.name === 'GameplayInfoButton'); cc.find = (relative, root) => relative.split('/').reduce((node, part) => node && node.getChildByName(part), root); cc.macro = { MAX_ZINDEX: 9999 }; cc.error = () => {}; let stops = 0, starts = 0, frozen = false; const map = { gameOver: false, shopPause: false, gameStart: true, pause: false, stopBoom() { stops++; }, startBoom() { starts++; }, iceTrue: () => frozen }; const managerExports = {}; const managerCode = ts.transpileModule(fs.readFileSync('assets/Script/SceneManager.ts', 'utf8'), { compilerOptions: { experimentalDecorators: true, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 } }).outputText; assert(!managerCode.includes("../GameplayIntro/script/GameplayIntro"), "Startup must not require the unloaded introduction bundle"); vm.runInNewContext(managerCode, { cc, exports: managerExports, require: request => request === './Map' ? { default: { _instance: map } } : modules[request.split('/').pop()] || {} }); const manager = new managerExports.default(); manager.node = canvas; manager.tipPanel = new Node('tipPanel'); manager.isPopupRuntimeAlive = () => true; manager.getDynamicPopup = () => null; manager.dynamicPopupLoads = {}; let popupLoads = 0, completeLoad; manager.loadDynamicPopup = (key, bundle, file, parent, callback) => { popupLoads++; manager.dynamicPopupLoads[key] = true; completeLoad = callback; }; manager.finishDynamicPopupLoad = key => delete manager.dynamicPopupLoads[key]; function setLevel(level) { Object.assign(cc.fx.GameConfig, JSON.parse(fs.readFileSync('assets/custom/Json/level' + level + '.json', 'utf8'))); manager.refreshGameplayInfo(); } setLevel(1); assert.equal(infoButton.active, false); manager.openGameplayIntro(); assert.equal(popupLoads, 0); assert.equal(stops, 0); assert.equal(map.pause, false); setLevel(511); assert.equal(infoButton.active, true); cc.fx.GameConfig.GM_INFO.level = 0; // Display follows the loaded map, not player progress. manager.openGameplayIntro(); assert.equal(popupLoads, 1); assert.equal(map.pause, true); assert.equal(stops, 1); assert.equal(manager.isPauseOpen(), true); manager.openGameplayIntro(); assert.equal(popupLoads, 1); ({ component } = setup(511)); component.close(); completeLoad(null, component.node); assert.equal(component.getPreviewState().total, 5); assert.equal(component.getPreviewState().index, 0); assert.equal(map.pause, true); component.close(); assert.equal(map.pause, false); assert.equal(starts, 1); manager.openGameplayIntro(); completeLoad(null, component.node); component.close(); assert.equal(starts, 2); manager.openGameplayIntro(); completeLoad(new Error('load failure')); assert.equal(map.pause, false); frozen = true; manager.openGameplayIntro(); completeLoad(null, component.node); component.close(); assert.equal(map.pause, true); frozen = false; console.log('PASS: real scene question visibility, empty-entry no-op, actual-map input, loading pause, close/reopen resume and frozen pause'); // Introduction metadata may finish loading after the game scene enters. let finishConfig; cc.resources = { load(_path, _type, callback) { finishConfig = callback; } }; cc.fx.GameConfig.NEW_LEVEL = null; manager.refreshGameplayInfo(); assert.equal(infoButton.active, false); finishConfig(null, { json: { NEW_LEVEL: config } }); assert.equal(infoButton.active, true); assert.equal(manager.levelGameplay.length, 5); manager.refreshGameplayInfo(); manager.isPopupRuntimeAlive = () => false; finishConfig(null, { json: { NEW_LEVEL: config } }); assert.equal(infoButton.active, false); console.log('PASS: delayed introduction metadata and discarded callback after scene exit');