d0b79dd0c9
Co-Authored-By: Claude Code <noreply@anthropic.com>
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
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);
|
||
});
|