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

287 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
});