287 lines
12 KiB
JavaScript
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);
|
||
}
|
||
});
|