Files
photo-live/apps/viewer/e2e/feed.spec.ts
T
2026-09-18 10:23:27 +08:00

133 lines
5.3 KiB
TypeScript
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.
import { expect, test, type Page } from '@playwright/test';
import { cleanupSeededAlbums, seedAlbum, seedCategory, seedPhoto } from './helpers';
// Task 7 瀑布流 Playwright(真实后端):35 张已发布照片(公开列表默认页 30)→
// 初始 30 + 滚动取第 2 页 5 张;分类 chips 过滤;新照片轮询提示(__viewerPollMs
// 注入缩短间隔,见 PhotoFeed pollIntervalMs 的 dev/test 覆盖钩子)。
test.afterAll(async () => {
await cleanupSeededAlbums();
});
/** 5 种宽高比例轮换(瀑布流高度预留的真实形状输入)。 */
const RATIOS: Array<{ width: number; height: number }> = [
{ width: 480, height: 300 },
{ width: 300, height: 480 },
{ width: 400, height: 400 },
{ width: 360, height: 480 },
{ width: 480, height: 270 },
];
const TOTAL = 35; // 30(第 1 页)+ 5(第 2 页)
/** 种「瀑布流相册」:2 分类 + 35 张已发布照片(前 20 张 catA、后 15 张 catB;
* publishedAt 逐张递减 60s 保证列表顺序确定)。 */
async function seedFeedAlbum(alias: string): Promise<{ albumId: string; token: string }> {
const album = await seedAlbum({
alias,
translations: [{ locale: 'zh-CN', title: '瀑布流测试相册' }],
});
const catA = await seedCategory(album.id, 0, [{ locale: 'zh-CN', name: '签到墙' }]);
const catB = await seedCategory(album.id, 1, [{ locale: 'zh-CN', name: '开场' }]);
const base = Date.now();
for (let i = 0; i < TOTAL; i++) {
const ratio = RATIOS[i % RATIOS.length]!;
await seedPhoto(album.id, `${alias}-p${i}`, {
categoryId: i < 20 ? catA : catB,
width: ratio.width,
height: ratio.height,
publishedAt: new Date(base - (i + 1) * 60_000),
});
}
return { albumId: album.id, token: album.publicToken };
}
/** 反复滚到底直至游标耗尽(end-note 出现)或循环上限。 */
async function scrollToLoadAll(page: Page): Promise<void> {
for (let i = 0; i < 15; i++) {
if ((await page.locator('.end-note').count()) > 0) return;
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(400);
}
}
test('scroll loads both pages until exhausted and thumbs decode', async ({ page }) => {
test.setTimeout(90_000);
const { token } = await seedFeedAlbum('scroll');
await page.addInitScript(() => localStorage.setItem('viewer.locale', 'zh-CN'));
await page.goto(`/a/${token}`);
// 第 1 页 30 张 + 哨兵在位(hasMore)。
await expect(page.locator('.cell')).toHaveCount(30);
await expect(page.locator('.sentinel')).toHaveCount(1);
// 懒加载:首屏缩略图经签名 URL 取回真实字节(解码成功 naturalWidth > 0)。
await expect
.poll(async () =>
page.locator('img.cell-img').first().evaluate((img) => (img as HTMLImageElement).naturalWidth),
)
.toBeGreaterThan(0);
await scrollToLoadAll(page);
// 第 2 页 5 张追加,游标耗尽 → 哨兵卸载、底部提示出现。
await expect(page.locator('.cell')).toHaveCount(35);
await expect(page.locator('.end-note')).toHaveText('已经到底了');
await expect(page.locator('.sentinel')).toHaveCount(0);
});
test('category chips filter the feed; All restores pagination', async ({ page }) => {
test.setTimeout(90_000);
const { token } = await seedFeedAlbum('filter');
await page.addInitScript(() => localStorage.setItem('viewer.locale', 'zh-CN'));
await page.goto(`/a/${token}`);
await expect(page.locator('.cell')).toHaveCount(30);
await page.getByRole('button', { name: '签到墙' }).click();
await expect(page.locator('.cell')).toHaveCount(20);
await expect(page.locator('.end-note')).toBeVisible(); // 20 ≤ 30 单页即尽
await page.getByRole('button', { name: '开场' }).click();
await expect(page.locator('.cell')).toHaveCount(15);
await page.getByRole('button', { name: '全部' }).click();
await expect(page.locator('.cell')).toHaveCount(30);
await scrollToLoadAll(page);
await expect(page.locator('.cell')).toHaveCount(35);
});
test('new-photo poll hint appears; click refreshes to include new photos', async ({ page }) => {
test.setTimeout(90_000);
const { albumId, token } = await seedFeedAlbum('poll');
await page.addInitScript(() => {
localStorage.setItem('viewer.locale', 'zh-CN');
(window as unknown as { __viewerPollMs?: number }).__viewerPollMs = 500;
});
await page.goto(`/a/${token}`);
await expect(page.locator('.cell')).toHaveCount(30);
await scrollToLoadAll(page);
await expect(page.locator('.cell')).toHaveCount(35);
await expect(page.locator('.new-photo-bar')).toHaveCount(0);
// 轮询期间直种 2 张更新的照片(publishedAt 晚于全部既有)。
const base = Date.now();
for (let i = 0; i < 2; i++) {
await seedPhoto(albumId, `poll-new-${i}`, {
categoryId: null,
width: 480,
height: 300,
publishedAt: new Date(base + (i + 1) * 60_000),
});
}
// 提示条出现(不自动插入——仍 35 张)。
await expect(page.locator('.new-photo-bar')).toBeVisible({ timeout: 10_000 });
await expect(page.locator('.cell')).toHaveCount(35);
// 点击 → 整表刷新回第 1 页(含新照片),滚动后 37 张。
await page.locator('.new-photo-bar').click();
await expect(page.locator('.new-photo-bar')).toHaveCount(0);
await scrollToLoadAll(page);
await expect(page.locator('.cell')).toHaveCount(37);
});