408 lines
26 KiB
JavaScript
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');
|