080dca8c4e
按容器宽度循环分配精选卡片到三列、两列或单列 使用 ResizeObserver 动态重排并保留 Grid 安全回退 补充布局临界值、分页追加和样式回归测试 同步创作主页文档与项目共享决策
154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
applySequentialShowcaseMasonry,
|
|
createSequentialShowcaseMasonryLayout,
|
|
resetSequentialShowcaseMasonry,
|
|
resolveShowcaseMasonryColumnCount,
|
|
} from './showcaseMasonryLayout';
|
|
|
|
function createRect(width: number, height: number): DOMRect {
|
|
return {
|
|
bottom: height,
|
|
height,
|
|
left: 0,
|
|
right: width,
|
|
top: 0,
|
|
width,
|
|
x: 0,
|
|
y: 0,
|
|
toJSON: () => ({}),
|
|
};
|
|
}
|
|
|
|
describe('showcase masonry layout', () => {
|
|
afterEach(() => {
|
|
document.body.replaceChildren();
|
|
});
|
|
|
|
it('assigns every group to columns in stable round-robin order', () => {
|
|
const layout = createSequentialShowcaseMasonryLayout({
|
|
containerWidth: 1296,
|
|
gap: 16,
|
|
itemHeights: [600, 300, 450, 200, 400, 250],
|
|
});
|
|
|
|
expect(layout.columnCount).toBe(3);
|
|
expect(layout.items.map((item) => item.columnIndex)).toEqual([
|
|
0, 1, 2, 0, 1, 2,
|
|
]);
|
|
expect(layout.items.map((item) => item.top)).toEqual([
|
|
0, 0, 0, 616, 316, 466,
|
|
]);
|
|
expect(layout.columnHeights).toEqual([816, 716, 716]);
|
|
expect(layout.containerHeight).toBe(816);
|
|
});
|
|
|
|
it('derives three, two, or one column from the actual container width', () => {
|
|
expect(
|
|
resolveShowcaseMasonryColumnCount({
|
|
containerWidth: 590.71,
|
|
gap: 14.72,
|
|
}),
|
|
).toBe(1);
|
|
expect(
|
|
resolveShowcaseMasonryColumnCount({
|
|
containerWidth: 590.72,
|
|
gap: 14.72,
|
|
}),
|
|
).toBe(2);
|
|
expect(
|
|
resolveShowcaseMasonryColumnCount({
|
|
containerWidth: 893.43,
|
|
gap: 14.72,
|
|
}),
|
|
).toBe(2);
|
|
expect(
|
|
resolveShowcaseMasonryColumnCount({
|
|
containerWidth: 893.44,
|
|
gap: 14.72,
|
|
}),
|
|
).toBe(3);
|
|
expect(
|
|
resolveShowcaseMasonryColumnCount({ containerWidth: 1600, gap: 14.72 }),
|
|
).toBe(3);
|
|
});
|
|
|
|
it('applies and resets absolute card positions without changing DOM order', () => {
|
|
const container = document.createElement('div');
|
|
container.className = 'creation-landing__asset-waterfall';
|
|
container.style.columnGap = '16px';
|
|
container.getBoundingClientRect = () => createRect(900, 0);
|
|
const heights = [600, 300, 450, 200, 400, 250];
|
|
const cards = heights.map((height, index) => {
|
|
const card = document.createElement('article');
|
|
card.className = 'creation-landing__asset-card';
|
|
card.textContent = `card-${index + 1}`;
|
|
card.getBoundingClientRect = () =>
|
|
createRect(Number.parseFloat(card.style.width) || 0, height);
|
|
container.append(card);
|
|
return card;
|
|
});
|
|
document.body.append(container);
|
|
|
|
const layout = applySequentialShowcaseMasonry(container);
|
|
|
|
expect(layout?.columnCount).toBe(3);
|
|
expect(container.dataset.masonryColumns).toBe('3');
|
|
expect(cards.map((card) => card.dataset.masonryColumn)).toEqual([
|
|
'1',
|
|
'2',
|
|
'3',
|
|
'1',
|
|
'2',
|
|
'3',
|
|
]);
|
|
expect(cards.map((card) => card.style.top)).toEqual([
|
|
'0px',
|
|
'0px',
|
|
'0px',
|
|
'616px',
|
|
'316px',
|
|
'466px',
|
|
]);
|
|
expect(
|
|
Array.from(container.children).map((card) => card.textContent),
|
|
).toEqual(['card-1', 'card-2', 'card-3', 'card-4', 'card-5', 'card-6']);
|
|
|
|
resetSequentialShowcaseMasonry(container);
|
|
|
|
expect(container.dataset.masonryColumns).toBeUndefined();
|
|
expect(container.style.height).toBe('');
|
|
expect(cards.every((card) => card.style.position === '')).toBe(true);
|
|
expect(cards.every((card) => card.style.top === '')).toBe(true);
|
|
});
|
|
|
|
it('keeps the flow fallback until every expected card has a valid height', () => {
|
|
const container = document.createElement('div');
|
|
container.className = 'creation-landing__asset-waterfall';
|
|
container.getBoundingClientRect = () => createRect(900, 0);
|
|
const card = document.createElement('article');
|
|
card.className = 'creation-landing__asset-card';
|
|
card.getBoundingClientRect = () => createRect(288, 0);
|
|
container.append(card);
|
|
document.body.append(container);
|
|
|
|
expect(
|
|
applySequentialShowcaseMasonry(container, { expectedItemCount: 2 }),
|
|
).toBeNull();
|
|
expect(
|
|
container.classList.contains(
|
|
'creation-landing__asset-waterfall--masonry',
|
|
),
|
|
).toBe(false);
|
|
|
|
expect(
|
|
applySequentialShowcaseMasonry(container, { expectedItemCount: 1 }),
|
|
).toBeNull();
|
|
expect(container.style.height).toBe('');
|
|
expect(card.style.position).toBe('');
|
|
});
|
|
});
|