MatchMaster/assets/test/tests/guide-carousel.cjs

408 lines
26 KiB
JavaScript

// Usage (project root): node assets/test/tests/guide-carousel.cjs <typescript.js>
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');