import { expect, test } from '@playwright/test'; import { cleanupSeededAlbums, flushSync, makePublicToken, seedAlbum, seedBannerAsset, seedCategory, } from './helpers'; // Task 6 活动页 Playwright(真实后端:cn API 8080 + ov API 8081 由 // playwright.config webServer 拉起或复用已在跑的实例;种子走 Prisma/MinIO 直种 // cn 侧,再 flushSync 经真实管线投影到 ov——viewer 已切 ov 读,见 helpers.ts)。 // 语言前置:无头 Chromium 默认 en-US——用 addInitScript 预置 localStorage,避免 // 依赖浏览器语言;zh 断言由此确定性地落在 zh-CN UI。 test.afterAll(async () => { await cleanupSeededAlbums(); }); test('renders album info in zh with timezone-adjusted time, chips and empty state', async ({ page }) => { const album = await seedAlbum({ alias: 'main', translations: [ { locale: 'zh-CN', title: '2026 公司年会', description: '年度盛会', venue: '大宴会厅', address: '上海市浦东新区世纪大道 100 号', }, { locale: 'en', title: 'Annual Party 2026', description: 'Yearly gala' }, ], }); await seedCategory(album.id, 0, [ { locale: 'zh-CN', name: '签到墙' }, { locale: 'en', name: 'Check-in Wall' }, ]); await seedCategory(album.id, 1, [{ locale: 'zh-CN', name: '开场' }]); await flushSync(album.id); await page.addInitScript(() => localStorage.setItem('viewer.locale', 'zh-CN')); await page.goto(`/a/${album.publicToken}`); await expect(page.locator('h1.title')).toHaveText('2026 公司年会'); await expect(page.locator('.description')).toHaveText('年度盛会'); // Asia/Shanghai:UTC 10-01 02:00/12:00 → 本地 10-01(不含 9/30 越日错误)。 const time = page.locator('.meta'); await expect(time).toContainText('10月1日'); await expect(time).not.toContainText('9月30'); await expect(page.locator('.meta')).toContainText('大宴会厅 · 上海市浦东新区世纪大道 100 号'); // Task 10 起排序(最新/热门)与分类 chips 并存——UI-02 V02 起排序为标签页 // (.sort-tabs .sort-tab),断言收窄到分类栏。 const chips = page.locator('.categories .chip'); await expect(chips).toHaveCount(3); // Task 7 起:全部 + 各分类(可点选) await expect(chips.first()).toBeEnabled(); await expect(chips).toHaveText(['全部', '签到墙', '开场']); await expect(page.locator('.sort-tabs .sort-tab')).toHaveText(['最新', '热门']); await expect(page.locator('.empty-notice')).toHaveText('照片即将上线'); // 该相册无 banner 资产 → 不渲染图。 await expect(page.locator('img.banner')).toHaveCount(0); await expect(page).toHaveTitle('2026 公司年会'); }); test('switching locale re-fetches and shows the English translation', async ({ page }) => { const album = await seedAlbum({ alias: 'switch', translations: [ { locale: 'zh-CN', title: '双语测试相册', description: '中文介绍' }, { locale: 'en', title: 'Bilingual Test Album', description: 'English intro', venue: 'Main Hall' }, ], }); await flushSync(album.id); await page.addInitScript(() => localStorage.setItem('viewer.locale', 'zh-CN')); await page.goto(`/a/${album.publicToken}`); await expect(page.locator('h1.title')).toHaveText('双语测试相册'); // UI-02 V01 紧凑语言控件:展开菜单后选择 English(两步)。 await page.locator('.locale-trigger').click(); await page.locator('.locale-option', { hasText: 'English' }).click(); await expect(page.locator('h1.title')).toHaveText('Bilingual Test Album'); await expect(page.locator('.description')).toHaveText('English intro'); await expect(page.locator('.meta')).toContainText('Main Hall'); await expect(page.locator('.empty-notice')).toHaveText('Photos coming soon'); await expect(page).toHaveTitle('Bilingual Test Album'); }); test('en UI with a zh-only album falls back to zh content while chrome stays en', async ({ page }) => { const album = await seedAlbum({ alias: 'fallback', translations: [{ locale: 'zh-CN', title: '只有中文的相册', venue: '大宴会厅' }], }); await flushSync(album.id); await page.addInitScript(() => localStorage.setItem('viewer.locale', 'en')); await page.goto(`/a/${album.publicToken}`); // 内容回退:resolvedLocale=zh-CN(服务端返回 zh 翻译)。 await expect(page.locator('h1.title')).toHaveText('只有中文的相册'); await expect(page.locator('.meta')).toContainText('大宴会厅'); // UI 语言仍为用户选择(en)。 await expect(page.locator('.empty-notice')).toHaveText('Photos coming soon'); }); test('banner signed URL loads a real image', async ({ page }) => { const album = await seedAlbum({ alias: 'banner', translations: [{ locale: 'zh-CN', title: '有封面的相册' }], }); await seedBannerAsset(album.id); await flushSync(album.id); await page.addInitScript(() => localStorage.setItem('viewer.locale', 'zh-CN')); await page.goto(`/a/${album.publicToken}`); const banner = page.locator('img.banner'); await expect(banner).toBeVisible(); // 签名 URL 可取回真实字节(加载完成 naturalWidth > 0;失败则保持 0)。 await expect .poll(async () => banner.evaluate((img) => (img as HTMLImageElement).naturalWidth)) .toBeGreaterThan(0); }); test('offline and ghost tokens show the localized unavailable error', async ({ page }) => { const offline = await seedAlbum({ alias: 'offline', status: 'offline', translations: [{ locale: 'zh-CN', title: '已下线相册' }], }); // 下线态也经真实管线同步:ov 侧 status=offline → isAlbumVisibleOv=false → 404。 await flushSync(offline.id); await page.addInitScript(() => localStorage.setItem('viewer.locale', 'zh-CN')); await page.goto(`/a/${offline.publicToken}`); await expect(page.locator('.error[role="alert"]')).toContainText('相册不存在或已下线'); await expect(page.locator('h1.title')).toHaveCount(0); // 幽灵 token:同码同文案(防枚举)。 await page.goto(`/a/${makePublicToken()}`); await expect(page.locator('.error[role="alert"]')).toContainText('相册不存在或已下线'); });