// 相册管理 e2e(真实后端 + compose 基础设施):登录 → 创建(zh+en)→ 列表可见 → // 编辑开关 → 标签更新;详情页:成员邀请(账号前缀搜索种子 staff)→ 分类创建 → // banner 直传(init→PUT→complete)预览 → 管理照片列表(种子照片状态)→ 隐藏。 // 选择器一律 data-test(test.use testIdAttribute),与文案解耦。 import { expect, test, type Page } from '@playwright/test' import { adminCredentials, loginAsAdmin, seedFixture } from './helpers' test.use({ testIdAttribute: 'data-test' }) test.beforeAll(() => { // 幂等种子 staff 账号(已存在则跳过;密码占位不可登录,仅用于邀请) seedFixture('staff', 'staff-e2e', 'Staff E2E') }) /** UI 创建相册(zh+en 标题),返回标题后缀 */ async function createAlbumViaUi(page: Page): Promise { const suffix = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) await page.getByTestId('album-create-btn').click() // UI06:创建改整页表单路由 await expect(page.getByTestId('album-form-page')).toBeVisible() await page.getByTestId('album-form-title-zh-CN').fill(`E2E相册 ${suffix}`) // 非激活页签的内容被隐藏(v-show):先切到英文页签再填写 await page.getByRole('tab', { name: '英文' }).click() await page.getByTestId('album-form-title-en').fill(`E2E Album ${suffix}`) // ElDatePicker 透传 attr 被 ElTooltip 吞掉:组件里用普通 div 挂 data-test,这里进 div 找 input await page.getByTestId('album-form-start-at').locator('input').fill('2026-10-01') // UI06:日期粒度 await page.keyboard.press('Enter') await page.getByTestId('album-form-submit').click() // 保存成功后跳相册详情;返回列表做断言 await expect(page.getByTestId('album-form-page')).toBeHidden() await page.goBack() // UI06:列表默认封面卡片网格;表格断言前先切换 await page.getByTestId('album-view-table').click() await expect(page.getByTestId('album-list-table').getByText(`E2E相册 ${suffix}`)).toBeVisible() return suffix } /** 列表内定位目标相册行 */ function albumRow(page: Page, suffix: string) { return page.getByTestId('album-list-table').locator('tr', { hasText: `E2E相册 ${suffix}` }) } test('创建相册(zh+en)→ 列表可见 → 关闭上传开关 → 标签更新', async ({ page }) => { await loginAsAdmin(page) const suffix = await createAlbumViaUi(page) const row = albumRow(page, suffix) await row.getByTestId('album-edit-btn').click() // UI06:编辑改整页表单(资料/访问页签) await expect(page.getByTestId('album-form-page')).toBeVisible() await page.getByRole('tab', { name: '访问' }).click() await page.getByTestId('album-form-switch-upload').click() await page.getByTestId('album-form-submit').click() await expect(page.getByTestId('album-form-page')).toBeHidden() await page.goBack() await page.getByTestId('album-view-table').click() await expect(albumRow(page, suffix).getByTestId('album-switch-upload')).toContainText('关') }) test('详情:邀请成员 → 建分类 → 传 banner → 照片管理(隐藏)', async ({ page }) => { await loginAsAdmin(page) const suffix = await createAlbumViaUi(page) // ── 进入详情 ── await albumRow(page, suffix).getByTestId('album-detail-btn').click() await expect(page.getByTestId('album-summary-cover')).toBeVisible() const albumId = /\/albums\/detail\/([^/?#]+)/.exec(page.url())?.[1] ?? '' expect(albumId).not.toBe('') // ── 成员:前缀搜索种子 staff 账号并邀请 ── await page.getByTestId('album-members-tab').click() const searchSelect = page.getByTestId('member-search-select') await searchSelect.click() await searchSelect.locator('input').fill('staff-e2e') await page.getByTestId('member-option-staff-e2e').click() await page.getByTestId('member-invite-btn').click() await expect(page.getByTestId('member-list-table').getByText('staff-e2e')).toBeVisible() // owner 徽标(admin 自己)可见 await expect( page.getByTestId('member-list-table').getByTestId('member-role-tag').first() ).toBeVisible() // ── 分类:zh/en 名称 ── await page.getByTestId('album-categories-tab').click() await page.getByTestId('category-create-btn').click() await page.getByTestId('category-form-name-zh').fill(`E2E分类 ${suffix}`) await page.getByTestId('category-form-name-en').fill(`E2E Category ${suffix}`) await page.getByTestId('category-form-submit').click() await expect(page.getByTestId('category-form-dialog')).toBeHidden() await expect(page.getByTestId('category-list-table').getByText(`E2E分类 ${suffix}`)).toBeVisible() // ── banner:真实小 jpeg 走 file chooser → init/PUT/complete → 预览 ── // UI06:banner 收进「品牌资产」页签 await page.getByTestId('album-assets-tab').click() const fileChooserPromise = page.waitForEvent('filechooser') await page.getByTestId('album-banner-pick').click() const fileChooser = await fileChooserPromise await fileChooser.setFiles({ name: 'banner-e2e.jpg', mimeType: 'image/jpeg', buffer: Buffer.from( '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwcJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPDs0NDT/wAALCAABAAEBAREA/8QAFAABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AmgA//9k=', 'base64' ) }) await expect(page.getByTestId('album-banner-status-done')).toBeVisible({ timeout: 20_000 }) await expect(page.getByTestId('album-banner-preview-img')).toBeVisible() // ── 照片管理:直种两种状态 → 列表可见 → 隐藏已发布照片 ── seedFixture('photos', albumId, adminCredentials().account) await page.reload() // UI06:照片默认网格视图;表格断言前先切换 await page.getByTestId('photo-view-table').click() await expect(page.getByTestId('photo-list-table')).toBeVisible() // 用处理状态标签(就绪)锚定行:隐藏后行的可见性文案变化,锚点保持稳定 const publishedRow = page .getByTestId('photo-list-table') .locator('tr') .filter({ has: page.getByTestId('photo-state-tag').filter({ hasText: '就绪' }) }) .first() await expect(publishedRow.getByTestId('photo-state-tag')).toHaveText('就绪') await expect(publishedRow.getByTestId('photo-visibility-tag')).toHaveText('已发布') await publishedRow.getByTestId('photo-hide-btn').click() await page.getByRole('button', { name: '确定' }).click() await expect(publishedRow.getByTestId('photo-visibility-tag')).toHaveText('已隐藏') // 处理失败状态标签也在列(种子第二行) await expect(page.getByTestId('photo-list-table').getByText('处理失败')).toBeVisible() })