369 lines
20 KiB
JavaScript
369 lines
20 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const { loadMembers, loadModule } = require('./new-player-ab-test-support.cjs');
|
|
const catalog = loadModule('assets/Script/module/Config/ShopCatalog.ts');
|
|
const minute = 60000;
|
|
const noon = Date.UTC(2026, 9, 12, 12);
|
|
const plain = value => JSON.parse(JSON.stringify(value));
|
|
|
|
function fixture({ deferCustomerService = false } = {}) {
|
|
let now = noon;
|
|
const storage = new Map(), listeners = new Map(), showListeners = new Set();
|
|
const calls = { coins: [], health: [], rewards: [], paid: [], ios: [], iosQueries: [], acknowledgements: [], queries: [] };
|
|
const overlays = { Loading: { active: false }, ConfirmBox: { active: false } };
|
|
const info = { uid: 'player', iosCanPay: true, iosStaminaPackOrder: '', shopDouble: {} };
|
|
const cc = {
|
|
fx: { GameConfig: { GM_INFO: info }, AudioManager: { _instance: { playEffect() {} } }, StorageMessage: {
|
|
getStorage: key => storage.get(key), setStorage: (key, value) => storage.set(key, plain(value)),
|
|
} },
|
|
isValid: value => !!value && !value.destroyed && !value.node?.destroyed,
|
|
find: () => null,
|
|
director: {
|
|
on: (event, cb, target) => listeners.set(event, cb.bind(target)),
|
|
off: event => listeners.delete(event),
|
|
emit: event => { const cb = listeners.get(event); if (cb) cb(); },
|
|
},
|
|
};
|
|
const DateMock = { now: () => now };
|
|
const state = loadModule('assets/Script/module/Config/StaminaPackState.ts', {}, { cc, Date: DateMock });
|
|
const sdk = { API: { showToast() {}, yinli_Pay() {}, shushu_SetSuperProperties() {} } };
|
|
const Tool = loadMembers('assets/Script/module/Tool/GameTool.ts', ['shopBuy'], {
|
|
cc, ...catalog, ...state, MiniGameSdk: sdk,
|
|
MapConroler: { _instance: { SceneManager: { openRewardWindow: data => calls.rewards.push(plain(data)) } } },
|
|
});
|
|
cc.fx.GameTool = Object.assign(new Tool(), {
|
|
changeCoin: n => calls.coins.push(n), setUserPowerTime: (...args) => calls.health.push(args),
|
|
shushu_Track() {}, tryReactivateStarterPack() {},
|
|
});
|
|
const Utils = {
|
|
buyProp: (...args) => { calls.paid.push(args.slice(0, 5)); calls.pay = args[5]; },
|
|
getPayInfo: (cb, order, options) => { calls.queries.push({ order, options }); calls.polledOrder = order; calls.poll = cb; },
|
|
setPayInfo: (cb, order) => { calls.acknowledgements.push(order); calls.ack = cb; },
|
|
GoKEFu: (data, cb) => {
|
|
calls.ios.push(data); info.iosStaminaPackOrder = 'ios-order'; calls.customerService = cb;
|
|
if (!deferCustomerService) cb('success');
|
|
},
|
|
getIosPayInfo: (order, cb) => { calls.iosQueries.push(order); calls.polledOrder = order; calls.iosPoll = cb; },
|
|
};
|
|
const Panel = loadMembers('assets/Script/StaminaPack.ts', [
|
|
'paying', 'checkingIosOrder', 'uid', 'pendingOrder', 'confirmedOrder', 'setPaying', 'refreshCountdown', 'closePop', 'closeAfterPurchase',
|
|
'buyProduct', 'iosOldPay', 'checkIosOrder', 'completePurchase', 'tryOpen',
|
|
'openConfirmBox', 'closeConfirmBox', 'againGet', 'checkNativeOrder', 'finishNativeOrder',
|
|
'onLoad', 'onDestroy', 'onShowListener',
|
|
], { cc, ...catalog, ...state, MiniGameSdk: sdk, Utils, Date: DateMock,
|
|
LoadingCatAnimation: { play: node => { node.active = true; }, stop: node => { node.active = false; } },
|
|
wx: {
|
|
getSystemInfoSync: () => ({ platform: calls.platform || 'android' }),
|
|
onShow: cb => showListeners.add(cb), offShow: cb => showListeners.delete(cb),
|
|
},
|
|
GameManager: { _instance: { staminaPackPrefab: {} } },
|
|
});
|
|
const panel = Object.assign(new Panel(), {
|
|
uid: info.uid, buyButton: { interactable: true, node: { on() {} } },
|
|
closeButton: { node: { on() {} } }, timeLabel: {}, schedule() {},
|
|
node: {
|
|
active: true, addComponent() {}, getChildByName: name => overlays[name],
|
|
destroy() { this.destroyed = true; panel.onDestroy(); },
|
|
},
|
|
});
|
|
listeners.set('stamina-pack-granted', () => panel.closeAfterPurchase());
|
|
return { state, info, calls, cc, panel, Panel, storage, overlays, setTime: t => { now = t; },
|
|
show: () => { for (const listener of showListeners) listener(); } };
|
|
}
|
|
|
|
test('first display starts 10-minute offer; reopening preserves deadline and survives module reload', () => {
|
|
const f = fixture();
|
|
assert.equal(f.state.canShowStaminaPack(), true);
|
|
f.state.beginStaminaPackOffer();
|
|
f.setTime(noon + 5 * minute);
|
|
f.state.beginStaminaPackOffer();
|
|
assert.deepEqual(plain(f.state.getStaminaPackRecord()), { offerEnd: noon + 10 * minute, cooldownEnd: noon + 40 * minute });
|
|
const reloaded = loadModule('assets/Script/module/Config/StaminaPackState.ts', {}, { cc: f.cc });
|
|
assert.equal(reloaded.canShowStaminaPack(noon + 9 * minute), true);
|
|
assert.equal(reloaded.canShowStaminaPack(noon + 10 * minute), false);
|
|
});
|
|
|
|
test('early close preserves timestamps; late close replaces cooldown with close time plus 30 minutes', () => {
|
|
const f = fixture(); f.state.beginStaminaPackOffer();
|
|
f.state.closeStaminaPackOffer(noon + 5 * minute);
|
|
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 40 * minute);
|
|
f.state.closeStaminaPackOffer(noon + 25 * minute);
|
|
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 55 * minute);
|
|
assert.equal(f.state.canShowStaminaPack(noon + 54 * minute), false);
|
|
assert.equal(f.state.canShowStaminaPack(noon + 55 * minute), true);
|
|
});
|
|
|
|
test('after an absent close, the preset cooldown expires; next actual trigger starts a fresh offer', () => {
|
|
const f = fixture(); f.state.beginStaminaPackOffer();
|
|
f.setTime(noon + 60 * minute);
|
|
assert.equal(f.state.canShowStaminaPack(), true);
|
|
f.state.beginStaminaPackOffer();
|
|
assert.equal(f.state.getStaminaPackRecord().offerEnd, noon + 70 * minute);
|
|
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 100 * minute);
|
|
});
|
|
|
|
test('zero countdown leaves the visible offer purchasable', () => {
|
|
const f = fixture(); f.state.beginStaminaPackOffer(); f.setTime(noon + 25 * minute);
|
|
f.panel.refreshCountdown();
|
|
assert.equal(f.panel.timeLabel.string, '00:00');
|
|
assert.equal(f.panel.node.destroyed, undefined);
|
|
f.panel.buyProduct();
|
|
assert.deepEqual(plain(f.calls.paid), [['StaminaPack', 1, 800, 'Android', 'StaminaPack']]);
|
|
});
|
|
|
|
test('native payment waits for acknowledgement, then closes before RewardWindow and clears offer time', () => {
|
|
const f = fixture(); f.state.beginStaminaPackOffer();
|
|
f.panel.buyProduct(); f.panel.buyProduct();
|
|
assert.equal(f.overlays.Loading.active, true);
|
|
assert.equal(f.calls.paid.length, 1);
|
|
assert.equal(f.calls.coins.length, 0);
|
|
f.calls.pay({ outTradeNo: 'order-1' });
|
|
assert.equal(f.calls.polledOrder, 'order-1');
|
|
f.calls.poll({ code: 1, data: { pay_state: 2 } });
|
|
assert.equal(f.overlays.Loading.active, true, 'loading covers delivery acknowledgement');
|
|
assert.deepEqual(f.calls.acknowledgements, ['order-1']);
|
|
assert.equal(f.calls.coins.length, 0);
|
|
f.setTime(noon + 11 * minute);
|
|
f.calls.ack({ code: 1 });
|
|
assert.equal(f.overlays.Loading.active, false);
|
|
assert.equal(f.panel.node.active, false);
|
|
assert.equal(f.panel.node.destroyed, true);
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
assert.deepEqual(f.calls.health, [[3600, 'StaminaPack', false]]);
|
|
assert.deepEqual(f.calls.rewards, [[{ type: 'coin', count: 1200 }, { type: 'infinite_health', count: 3600 }]]);
|
|
assert.deepEqual(plain(f.state.getStaminaPackRecord()), { offerEnd: 0, cooldownEnd: noon + 491 * minute });
|
|
f.state.closeStaminaPackOffer(noon + 12 * minute);
|
|
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 491 * minute);
|
|
});
|
|
|
|
test('query timeout shows confirmation and retries only the original order without duplicate payment', () => {
|
|
const f = fixture(); f.panel.buyProduct(); f.calls.pay({ outTradeNo: 'retry-order' });
|
|
assert.equal(f.calls.queries[0].options.timeoutMs, 30000);
|
|
f.calls.poll({ code: 0, data: { pay_state: -1 } });
|
|
assert.equal(f.overlays.Loading.active, false);
|
|
assert.equal(f.overlays.ConfirmBox.active, true);
|
|
assert.equal(f.calls.coins.length, 0);
|
|
f.panel.againGet(); f.panel.againGet(); f.panel.buyProduct();
|
|
assert.equal(f.overlays.Loading.active, true);
|
|
assert.equal(f.overlays.ConfirmBox.active, false);
|
|
assert.deepEqual(f.calls.queries.map(q => q.order), ['retry-order', 'retry-order']);
|
|
assert.equal(f.calls.paid.length, 1);
|
|
f.calls.poll({ code: 1, data: { pay_state: 2 } }); f.calls.ack({ code: 1 });
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
});
|
|
|
|
test('failed delivery acknowledgement retries delivery without requerying or creating an order', () => {
|
|
const f = fixture(); f.panel.buyProduct(); f.calls.pay({ outTradeNo: 'grant-retry' });
|
|
f.calls.poll({ code: 1, data: { pay_state: 2 } }); f.calls.ack({ code: 0 });
|
|
assert.equal(f.overlays.ConfirmBox.active, true);
|
|
assert.equal(f.calls.coins.length, 0);
|
|
f.panel.againGet(); f.panel.againGet();
|
|
assert.equal(f.overlays.Loading.active, true);
|
|
assert.deepEqual(f.calls.acknowledgements, ['grant-retry', 'grant-retry']);
|
|
assert.equal(f.calls.queries.length, 1);
|
|
assert.equal(f.calls.paid.length, 1);
|
|
f.calls.ack({ code: 1 });
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
});
|
|
|
|
test('legacy iOS timeout and network error retain the order for confirmation retry', () => {
|
|
for (const response of [{ code: 2 }, { code: 0, networkError: true }]) {
|
|
const f = fixture(); f.info.iosStaminaPackOrder = 'ios-retry';
|
|
f.panel.checkIosOrder();
|
|
assert.equal(f.overlays.Loading.active, true);
|
|
f.calls.iosPoll(response);
|
|
assert.equal(f.info.iosStaminaPackOrder, 'ios-retry');
|
|
assert.equal(f.overlays.Loading.active, false);
|
|
assert.equal(f.overlays.ConfirmBox.active, true);
|
|
f.panel.againGet();
|
|
assert.equal(f.calls.polledOrder, 'ios-retry');
|
|
assert.equal(f.overlays.ConfirmBox.active, false);
|
|
assert.equal(f.overlays.Loading.active, true);
|
|
f.calls.iosPoll({ code: 1, data: { payment_name: 'StaminaPack' } });
|
|
assert.equal(f.info.iosStaminaPackOrder, '');
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
assert.equal(f.calls.paid.length + f.calls.ios.length, 0);
|
|
}
|
|
});
|
|
|
|
test('compensation starts eight hours at grant, duplicate grant cannot extend cooldown or grant twice', () => {
|
|
const f = fixture(); f.setTime(noon + 60 * minute);
|
|
f.cc.fx.GameTool.shopBuy('StaminaPack', true, { outTradeNo: 'pending' });
|
|
f.setTime(noon + 120 * minute);
|
|
f.cc.fx.GameTool.shopBuy('StaminaPack', true, { outTradeNo: 'pending' });
|
|
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 540 * minute);
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
assert.deepEqual(f.calls.health, [[3600, 'StaminaPack', true]]);
|
|
assert.equal(f.calls.rewards.length, 1);
|
|
assert.equal(f.state.canShowStaminaPack(noon + 539 * minute), false);
|
|
assert.equal(f.state.canShowStaminaPack(noon + 540 * minute), true);
|
|
});
|
|
|
|
test('cancel, unconfirmed payment and failed acknowledgement never start purchase cooldown', () => {
|
|
for (const stage of ['cancel', 'unconfirmed', 'ack']) {
|
|
const f = fixture(); f.state.beginStaminaPackOffer(); f.panel.buyProduct();
|
|
if (stage === 'cancel') f.calls.pay({ err: true, errCode: -2 });
|
|
else {
|
|
f.calls.pay({ outTradeNo: 'failed' });
|
|
f.calls.poll({ code: 1, data: { pay_state: stage === 'ack' ? 2 : 1 } });
|
|
if (stage === 'ack') f.calls.ack({ code: 0 });
|
|
}
|
|
assert.equal(f.state.getStaminaPackRecord().offerEnd, noon + 10 * minute);
|
|
assert.equal(f.calls.coins.length, 0);
|
|
assert.equal(f.panel.buyButton.interactable, true);
|
|
assert.equal(f.overlays.Loading.active, false);
|
|
assert.equal(f.overlays.ConfirmBox.active, stage === 'ack');
|
|
}
|
|
});
|
|
|
|
test('closing before acknowledgement does not lose delivery or overwrite its eight-hour cooldown', () => {
|
|
const f = fixture(); f.state.beginStaminaPackOffer(); f.panel.buyProduct();
|
|
f.calls.pay({ outTradeNo: 'late' }); f.calls.poll({ code: 1, data: { pay_state: 2 } });
|
|
f.setTime(noon + 25 * minute); f.panel.closePop();
|
|
f.setTime(noon + 26 * minute); f.calls.ack({ code: 1 });
|
|
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 506 * minute);
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
});
|
|
|
|
test('legacy iOS uses existing customer service and query; success grants the same product', () => {
|
|
const f = fixture(); f.calls.platform = 'ios'; f.info.iosCanPay = false;
|
|
f.state.beginStaminaPackOffer(); f.panel.buyProduct();
|
|
assert.deepEqual(plain(f.calls.ios), [{ price: 800, payment_name: 'StaminaPack', payment_count: 1 }]);
|
|
f.panel.checkIosOrder(); f.panel.checkIosOrder();
|
|
f.calls.iosPoll({ code: 1, data: { payment_name: 'StaminaPack' } });
|
|
assert.equal(f.info.iosStaminaPackOrder, '');
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
assert.equal(f.state.getStaminaPackRecord().offerEnd, 0);
|
|
});
|
|
|
|
for (const fallback of [false, true]) {
|
|
for (const callbackFirst of [false, true]) {
|
|
test(`legacy iOS return delivers once: ${fallback ? 'error 16 fallback' : 'direct customer service'}, ${callbackFirst ? 'callback first' : 'onShow first'}`, () => {
|
|
const f = fixture({ deferCustomerService: true });
|
|
f.calls.platform = 'ios'; f.info.iosCanPay = fallback;
|
|
f.panel.onLoad(); f.panel.buyProduct();
|
|
if (fallback) f.calls.pay({ errCode: 16, outTradeNo: 'native-failed' });
|
|
assert.equal(f.calls.ios.length, 1);
|
|
assert.equal(f.panel.pendingOrder, '');
|
|
assert.equal(f.overlays.Loading.active, true);
|
|
assert.deepEqual(f.calls.iosQueries, []);
|
|
if (callbackFirst) {
|
|
f.calls.customerService('success');
|
|
assert.deepEqual(f.calls.iosQueries, [], 'opening customer service must not query before payment');
|
|
}
|
|
f.show();
|
|
assert.deepEqual(f.calls.iosQueries, ['ios-order']);
|
|
if (!callbackFirst) f.calls.customerService('success');
|
|
assert.equal(f.panel.buyButton.interactable, false, 'late customer callback cannot unlock a running query');
|
|
assert.equal(f.overlays.Loading.active, true);
|
|
f.show(); f.panel.buyProduct();
|
|
assert.deepEqual(f.calls.iosQueries, ['ios-order'], 'foreground and button events cannot duplicate the query');
|
|
f.calls.iosPoll({ code: 1, data: { payment_name: 'StaminaPack' } });
|
|
f.show();
|
|
assert.equal(f.info.iosStaminaPackOrder, '');
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
assert.deepEqual(f.calls.health, [[3600, 'StaminaPack', false]]);
|
|
assert.equal(f.calls.rewards.length, 1);
|
|
assert.equal(f.calls.ios.length, 1);
|
|
assert.equal(f.calls.paid.length, fallback ? 1 : 0);
|
|
});
|
|
}
|
|
}
|
|
|
|
test('legacy iOS return retains a network-failed order and releases the query lock for retry', () => {
|
|
const f = fixture({ deferCustomerService: true });
|
|
f.calls.platform = 'ios'; f.info.iosCanPay = false;
|
|
f.panel.onLoad(); f.panel.buyProduct(); f.show();
|
|
assert.equal(f.calls.iosQueries.length, 1);
|
|
f.calls.customerService('success');
|
|
f.calls.iosPoll({ code: 0, networkError: true });
|
|
assert.equal(f.info.iosStaminaPackOrder, 'ios-order');
|
|
assert.equal(f.overlays.ConfirmBox.active, true);
|
|
assert.equal(f.panel.buyButton.interactable, true);
|
|
f.panel.againGet(); f.show();
|
|
assert.deepEqual(f.calls.iosQueries, ['ios-order', 'ios-order']);
|
|
f.calls.iosPoll({ code: 1, data: { payment_name: 'StaminaPack' } });
|
|
assert.deepEqual(f.calls.coins, [1200]);
|
|
});
|
|
|
|
test('failed customer service opening restores the button without querying or granting', () => {
|
|
const f = fixture({ deferCustomerService: true });
|
|
f.calls.platform = 'ios'; f.info.iosCanPay = false;
|
|
f.panel.onLoad(); f.panel.buyProduct(); f.calls.customerService('fail'); f.show();
|
|
assert.equal(f.info.iosStaminaPackOrder, '');
|
|
assert.equal(f.panel.buyButton.interactable, true);
|
|
assert.equal(f.overlays.Loading.active, false);
|
|
assert.deepEqual(f.calls.iosQueries, []);
|
|
assert.deepEqual(f.calls.coins, []);
|
|
});
|
|
|
|
test('an old account panel cannot query another account order or grant after an account switch', () => {
|
|
const f = fixture(); f.panel.onLoad();
|
|
f.info.uid = 'other'; f.info.iosStaminaPackOrder = 'other-order';
|
|
f.show();
|
|
assert.deepEqual(f.calls.iosQueries, []);
|
|
f.info.uid = 'player'; f.info.iosStaminaPackOrder = 'player-order';
|
|
f.show();
|
|
assert.equal(f.panel.checkingIosOrder, true);
|
|
f.info.uid = 'other'; f.info.iosStaminaPackOrder = 'other-order';
|
|
f.calls.iosPoll({ code: 1, data: { payment_name: 'StaminaPack' } });
|
|
assert.equal(f.panel.checkingIosOrder, false);
|
|
assert.equal(f.info.iosStaminaPackOrder, 'other-order');
|
|
assert.deepEqual(f.calls.coins, []);
|
|
});
|
|
|
|
test('a changed account ignores the outstanding purchase callback', () => {
|
|
const f = fixture(); f.panel.buyProduct(); f.info.uid = 'other';
|
|
f.calls.pay({ outTradeNo: 'old-account' });
|
|
assert.equal(f.calls.polledOrder, undefined);
|
|
assert.equal(f.calls.coins.length, 0);
|
|
});
|
|
|
|
test('home close triggers only without successful coin refill; programmatic start-game close does not', () => {
|
|
let opened = 0;
|
|
const cc = { find: () => null, fx: { AudioManager: { _instance: { playEffect() {} } } } };
|
|
const Health = loadMembers('assets/Script/heathnum.ts', ['boughtHealth', 'closePop'], {
|
|
cc, StaminaPack: { tryOpen: () => opened++ }, console: { warn() {} },
|
|
});
|
|
const h = new Health(); h.heath = { destroy() {} };
|
|
h.closePop(); assert.equal(opened, 0);
|
|
h.closePop({}); assert.equal(opened, 1);
|
|
h.boughtHealth = true; h.closePop({}); assert.equal(opened, 1);
|
|
});
|
|
|
|
test('game retry keeps toast, selects pack when eligible, otherwise uses the original shop', async () => {
|
|
for (const eligible of [true, false]) {
|
|
let shops = 0, packs = 0, toasts = 0;
|
|
const cc = { fx: { AudioManager: { _instance: { playEffect() {} } },
|
|
GameTool: { getUserPowerTime: () => false }, GameConfig: { GM_INFO: { hp: 0 } } } };
|
|
const MapConroler = { _instance: { powerState: false } };
|
|
const Map = loadMembers('assets/Script/Map.ts', ['againLevel'], {
|
|
cc, MapConroler, StaminaPack: { tryOpen() { packs++; return eligible; } },
|
|
MiniGameSdk: { API: { showToast: () => toasts++ } }, setTimeout: cb => cb(),
|
|
});
|
|
const map = Object.assign(new Map(), { isMapRuntimeAlive: () => true, openShop: () => shops++ });
|
|
await map.againLevel();
|
|
assert.equal(toasts, 1); assert.equal(packs, 1); assert.equal(shops, eligible ? 0 : 1);
|
|
}
|
|
});
|
|
|
|
test('prefab bindings resolve the authored countdown/buttons and startup references the prefab', () => {
|
|
const prefab = JSON.parse(fs.readFileSync('assets/prefab/pop/heathpop2.prefab'));
|
|
const component = prefab.find(v => v.timeLabel && v.buyButton && v.closeButton);
|
|
assert.equal(prefab[component.timeLabel.__id__].__type__, 'cc.Label');
|
|
assert.equal(prefab[component.buyButton.__id__].__type__, 'cc.Button');
|
|
assert.equal(prefab[prefab[component.closeButton.__id__].node.__id__]._name, 'close');
|
|
const loading = prefab.find(v => v._name === 'Loading');
|
|
const confirm = prefab.find(v => v._name === 'ConfirmBox');
|
|
assert.equal(loading._active, false); assert.equal(confirm._active, false);
|
|
assert.equal(loading._parent.__id__, component.node.__id__);
|
|
assert.equal(confirm._parent.__id__, component.node.__id__);
|
|
const retry = prefab.find(v => v.__type__ === 'cc.ClickEvent' && v.handler === 'againGet');
|
|
assert.equal(retry.target.__id__, component.node.__id__);
|
|
assert.equal(retry._componentId, component.__type__);
|
|
const scene = JSON.parse(fs.readFileSync('assets/StartScene/LoadScene.fire'));
|
|
const meta = JSON.parse(fs.readFileSync('assets/prefab/pop/heathpop2.prefab.meta'));
|
|
assert.equal(scene.find(v => v.staminaPackPrefab).staminaPackPrefab.__uuid__, meta.uuid);
|
|
});
|