Files
MatchMaster/tools/test-shop-activity-carousel.cjs
2026-09-29 18:57:56 +08:00

287 lines
12 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const ts = require('typescript');
function setup() {
const prefab = JSON.parse(fs.readFileSync('assets/shop/prefab/shop2.prefab', 'utf8'));
const sourceActivity = prefab.find(n => n.__type__ === 'cc.Node' && n._name === 'activity');
const sourceProgress = prefab.find(n => n.__type__ === 'cc.Node' && n._name === 'progress');
function node(raw, parent) {
const result = {
name: raw._name, x: raw._trs.array[0], scaleX: raw._trs.array[7],
width: raw._contentSize.width, anchorX: raw._anchorPoint.x,
active: raw._active, parent, children: [], on() {}, off() {},
getChildByName(name) { return this.children.find(child => child.name === name) || null; },
setSiblingIndex(index) {
const siblings = this.parent.children;
siblings.splice(siblings.indexOf(this), 1);
siblings.splice(index, 0, this);
}
};
result.children = raw._children.map(ref => node(prefab[ref.__id__], result));
return result;
}
const parent = { convertToNodeSpaceAR: p => p };
const activity = node(sourceActivity, parent);
const progress = node(sourceProgress, parent);
const selects = progress.children.map(dot => dot.getChildByName('select'));
const sourceLayout = sourceActivity._components.map(ref => prefab[ref.__id__])
.find(component => component.__type__ === 'cc.Layout');
let layoutUpdates = 0;
const layout = { updateLayout() {
layoutUpdates++;
const children = activity.children.filter(child => child.active);
const spacing = sourceLayout['_N$spacingX'];
activity.width = children.reduce((width, child) => width + child.width, 0)
+ spacing * Math.max(0, children.length - 1);
let left = -activity.width * activity.anchorX;
children.forEach(child => {
child.x = left + child.width * child.anchorX;
left += child.width + spacing;
});
} };
activity.getComponent = () => layout;
const cc = {
_decorator: { ccclass: value => value }, Component: function() {}, Layout: function() {},
Node: { EventType: { TOUCH_START: 'start', TOUCH_MOVE: 'move', TOUCH_END: 'end', TOUCH_CANCEL: 'cancel' } },
Event: { EventTouch: function(touches, bubbles) { this.bubbles = bubbles; this.getID = () => 1; } },
find: path => {
let raw = prefab[prefab[0].data.__id__];
for (const name of path.split('/')) {
raw = raw && raw._children.map(ref => prefab[ref.__id__]).find(node => node._name === name);
}
return raw === sourceActivity ? activity : raw === sourceProgress ? progress : null;
},
tween: node => ({ to(duration, props) { this.props = props; return this; },
start() { Object.assign(node, this.props); return this; }, stop() {} })
};
cc.Event.EventTouch.ENDED = 2;
const context = { cc, exports: {} };
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/shop/script/ShopActivityCarousel.ts', 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, experimentalDecorators: true }
}).outputText, context);
const carousel = new context.exports.default();
carousel.node = { on() {}, off() {} };
carousel.onLoad();
let cancellations = 0;
const button = { parent: activity, dispatchEvent(event) {
cancellations++;
carousel.onTouchCancel(event);
} };
function event(x = 0, y = 0, id = 1) {
return { target: button, touch: {}, bubbles: true, stopped: false,
getEventCode: () => 3,
getID: () => id, getTouches: () => [], getStartLocation: () => ({ x: 0, y: 0 }),
getLocation: () => ({ x, y, sub: p => ({ x: x - p.x, y: y - p.y }) }),
stopPropagation() { this.stopped = true; } };
}
function swipe(x, y = 0) {
carousel.onTouchStart(event());
const move = event(x, y), end = event(x, y);
carousel.onTouchMove(move);
carousel.onTouchEnd(end);
return { move, end };
}
return { carousel, activity, progress, selects, event, swipe,
cancellations: () => cancellations, layoutUpdates: () => layoutUpdates };
}
function visibleCards(s) {
return s.activity.children.filter(node => node.active);
}
function centerCard(s) {
return visibleCards(s).find(node => s.activity.x + node.x * s.activity.scaleX === 0)?.name;
}
const combinations = [
{ names: [], pages: [], selected: -1 },
{ names: ['newbieGift'], pages: ['newbieGift'], selected: 0 },
{ names: ['jungle'], pages: ['jungle'], selected: 0 },
{ names: ['banre'], pages: ['banre'], selected: 0 },
{ names: ['newbieGift', 'jungle'], pages: ['newbieGift', 'jungle'], selected: 0 },
{ names: ['newbieGift', 'banre'], pages: ['newbieGift', 'banre'], selected: 0 },
{ names: ['jungle', 'banre'], pages: ['jungle', 'banre'], selected: 0 },
{ names: ['newbieGift', 'jungle', 'banre'], pages: ['jungle', 'newbieGift', 'banre'], selected: 1 }
];
for (const { names, pages, selected } of combinations) {
test(`活动可用组合 ${names.join('+') || '无'}:最高优先级居中,分页点数量和位置匹配`, () => {
const s = setup();
s.carousel.setEntries(names);
const cards = visibleCards(s);
assert.deepEqual(cards.map(node => node.name), pages);
assert.equal(s.activity.active, names.length > 0);
assert.equal(s.progress.active, names.length > 0);
assert.equal(centerCard(s), names[0]);
assert.deepEqual(s.progress.children.map(dot => dot.active), [0, 1, 2].map(i => i < names.length));
assert.deepEqual(s.progress.children.filter(dot => dot.active).map(dot => dot.x),
names.map((_, i) => (i - (names.length - 1) / 2) * 50));
assert.deepEqual(s.selects.map(select => select.active), [0, 1, 2].map(i => i === selected));
for (let i = 1; i < cards.length; i++) {
assert.equal(cards[i].x - cards[i - 1].x,
(cards[i].width + cards[i - 1].width) / 2 + 40);
}
if (names.length === 2) {
assert.ok(s.activity.x + cards[1].x > 0);
s.swipe(20);
assert.equal(centerCard(s), names[0], '初始页向右滑不会进入隐藏节点');
s.swipe(-20);
assert.equal(centerCard(s), names[1]);
assert.deepEqual(s.selects.map(select => select.active), [false, true, false]);
s.swipe(-20);
assert.equal(centerCard(s), names[1], '末页向左滑不会进入隐藏节点');
s.swipe(20);
assert.equal(centerCard(s), names[0]);
}
});
}
test('同一活动集合重复刷新保留选择;显式 reset 恢复优先级入口', () => {
const s = setup();
s.swipe(-20);
assert.equal(centerCard(s), 'banre');
const updates = s.layoutUpdates();
s.carousel.setEntries(['newbieGift', 'jungle', 'banre']);
assert.equal(centerCard(s), 'banre');
assert.equal(s.layoutUpdates(), updates);
s.carousel.setEntries(['newbieGift', 'jungle', 'banre'], true);
assert.equal(centerCard(s), 'newbieGift');
assert.equal(s.layoutUpdates(), updates + 1);
});
test('拖动中移除、清空并恢复入口:不留下隐藏页,也不会被旧手势带走', () => {
const s = setup();
s.swipe(-20);
s.carousel.onTouchStart(s.event());
s.carousel.onTouchMove(s.event(100));
s.carousel.setEntries(['newbieGift', 'jungle']);
s.carousel.onTouchEnd(s.event(100));
assert.equal(centerCard(s), 'newbieGift');
assert.deepEqual(visibleCards(s).map(node => node.name), ['newbieGift', 'jungle']);
s.swipe(-20);
assert.equal(centerCard(s), 'jungle');
s.carousel.setEntries(['banre']);
assert.equal(centerCard(s), 'banre');
s.carousel.setEntries([]);
assert.equal(s.activity.x, 0);
assert.deepEqual(s.selects.map(select => select.active), [false, false, false]);
s.carousel.setEntries(['newbieGift', 'jungle', 'banre']);
assert.deepEqual(visibleCards(s).map(node => node.name), ['jungle', 'newbieGift', 'banre']);
assert.equal(centerCard(s), 'newbieGift');
assert.deepEqual(s.activity.children.map(node => node.x), [-906, 0, 906]);
assert.deepEqual(s.progress.children.map(dot => dot.x), [-50, 0, 50]);
});
test('零页和单页不截获横滑、点击或纵向手势', () => {
for (const names of [[], ['newbieGift'], ['jungle'], ['banre']]) {
const s = setup();
s.carousel.setEntries(names);
const x = s.activity.x;
for (const [offsetX, offsetY] of [[200, 0], [-200, 0], [0, 0], [20, 200]]) {
const { move, end } = s.swipe(offsetX, offsetY);
assert.equal(move.stopped, false);
assert.equal(end.stopped, false);
assert.equal(s.activity.x, x);
}
assert.equal(s.cancellations(), 0);
}
});
test('关闭时把拖动位置归位到当前页,重新启用保留选中的可用入口', () => {
const s = setup();
s.carousel.setEntries(['jungle', 'banre']);
s.swipe(-20);
s.carousel.onTouchStart(s.event());
s.carousel.onTouchMove(s.event(100));
s.carousel.onDisable();
assert.equal(centerCard(s), 'banre');
assert.deepEqual(s.selects.map(select => select.active), [false, true, false]);
s.carousel.onEnable();
s.swipe(20);
assert.equal(centerCard(s), 'jungle');
});
test('从中间开始;右滑左卡居中,左滑恢复,并且只亮一个 select', () => {
const s = setup();
assert.equal(s.activity.x, 0);
assert.deepEqual(s.selects.map(n => n.active), [false, true, false]);
s.swipe(20);
assert.equal(s.activity.x, 906);
assert.deepEqual(s.selects.map(n => n.active), [true, false, false]);
s.swipe(-20);
assert.equal(s.activity.x, 0);
s.swipe(-200);
assert.equal(s.activity.x, -906);
assert.deepEqual(s.selects.map(n => n.active), [false, false, true]);
});
test('每次最多翻一页,首尾不越界', () => {
const s = setup();
s.swipe(4000);
s.swipe(4000);
assert.equal(s.activity.x, 906);
s.swipe(-4000);
assert.equal(s.activity.x, 0);
s.swipe(-4000);
s.swipe(-4000);
assert.equal(s.activity.x, -906);
});
test('横拖跟手,取消按钮触摸并拦截松手,短拖回原页', () => {
const s = setup();
s.carousel.onTouchStart(s.event());
const move = s.event(19);
s.carousel.onTouchMove(move);
assert.equal(s.activity.x, 19);
assert.equal(s.cancellations(), 1);
assert.equal(move.stopped, true);
const end = s.event(19);
s.carousel.onTouchEnd(end);
assert.equal(end.stopped, true);
assert.equal(s.activity.x, 0);
});
test('轻点保留按钮事件;纵向滑动交给外层滚动,不翻页', () => {
const s = setup();
s.carousel.onTouchStart(s.event());
const tap = s.event();
s.carousel.onTouchEnd(tap);
assert.equal(tap.stopped, false);
const { move, end } = s.swipe(20, 200);
assert.equal(move.stopped, false);
assert.equal(end.stopped, false);
assert.equal(s.cancellations(), 0);
assert.equal(s.activity.x, 0);
});
test('系统取消横拖回原页;忽略 ScrollView 模拟取消和其他手指', () => {
const s = setup();
s.carousel.onTouchStart(s.event());
s.carousel.onTouchMove(s.event(200, 0, 2));
assert.equal(s.activity.x, 0);
s.carousel.onTouchCancel(Object.assign(s.event(), { simulate: true }));
s.carousel.onTouchMove(s.event(200));
assert.equal(s.activity.x, 200);
s.carousel.onTouchCancel(s.event(200));
assert.equal(s.activity.x, 0);
assert.deepEqual(s.selects.map(n => n.active), [false, true, false]);
});
test('滑出原按钮区域后松手:TOUCH_CANCEL 携带 ENDED 仍应翻页', () => {
for (const offset of [200, -200]) {
const s = setup();
s.carousel.onTouchStart(s.event());
s.carousel.onTouchMove(s.event(offset));
const releaseOutside = s.event(offset);
releaseOutside.getEventCode = () => 2;
s.carousel.onTouchCancel(releaseOutside);
assert.equal(s.activity.x, offset > 0 ? 906 : -906);
assert.deepEqual(s.selects.map(n => n.active), offset > 0
? [true, false, false] : [false, false, true]);
assert.equal(releaseOutside.stopped, true);
}
});