e44b35db28
- 管理后台:DatePicker type=datetime → date;提交归一(开始=当天 00:00、 结束=当天 23:59 整天活动语义;契约仍 datetime 仅交互粒度) - App:去掉时间选择步骤(选日期即定;同上归一);表单显示只显 yyyy/M/d - e2e 日期填入改 '2026-10-01'(date 格式);widget 断言日期粒度 - admin 87/87 + build、app 397/397 全绿 Co-Authored-By: Claude Code <noreply@anthropic.com>
289 lines
13 KiB
TypeScript
289 lines
13 KiB
TypeScript
// 相册治理增量 e2e(P4a Task 9,真实后端 + compose 基础设施):批量隐藏
|
||
// (多选 2 → toast 成功汇总 + 行原位已隐藏 + cn 公开列表仅剩 1)→ 负责人交接
|
||
// (前缀搜索 staff → API 断言 ownerId 变更)→ 水印(配置保存 → 任务受理 →
|
||
// fixture 直驱 worker 核心到 done → UI 轮询见已完成 + 公开 browse URL 指向
|
||
// /watermark.jpg)→ 用量(阈值调小触发告警 → 上调解除)→ 分享二维码
|
||
// (canvas 非空 + URL 复制)。选择器一律 data-test;文案断言仅用稳定短语。
|
||
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 标题),返回标题后缀(同 albums.spec 模式) */
|
||
async function createAlbumViaUi(page: Page): Promise<string> {
|
||
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}`)
|
||
await page.getByRole('tab', { name: '英文' }).click()
|
||
await page.getByTestId('album-form-title-en').fill(`E2E Gov Album ${suffix}`)
|
||
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}` })
|
||
}
|
||
|
||
/** 进入详情页,返回 albumId */
|
||
async function openAlbumDetail(page: Page, suffix: string): Promise<string> {
|
||
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('')
|
||
return albumId
|
||
}
|
||
|
||
/** API 登录(UI 之外的断言用):返回 Bearer 会话令牌 */
|
||
async function apiLogin(page: Page): Promise<string> {
|
||
const { account, password } = adminCredentials()
|
||
const res = await page.request.post('/v1/auth/password', { data: { account, password } })
|
||
expect(res.ok()).toBeTruthy()
|
||
const body = (await res.json()) as { sessionToken: string }
|
||
return body.sessionToken
|
||
}
|
||
|
||
/** cn 公开列表条数(批量隐藏的观众侧断言) */
|
||
async function publicPhotoCount(page: Page, token: string): Promise<number> {
|
||
const res = await page.request.get(`/v1/public/albums/${token}/photos`)
|
||
expect(res.ok()).toBeTruthy()
|
||
const body = (await res.json()) as { items: unknown[] }
|
||
return body.items.length
|
||
}
|
||
|
||
/** ElSwitch 幂等设到目标态(is-checked 类为准;已同态不点击)。 */
|
||
async function setSwitchState(page: Page, testId: string, on: boolean): Promise<void> {
|
||
const isOn = await page.getByTestId(testId).evaluate((el) => el.classList.contains('is-checked'))
|
||
if (isOn !== on) {
|
||
await page.getByTestId(testId).click()
|
||
}
|
||
}
|
||
|
||
test('批量隐藏:多选 2 → 汇总 toast + 行原位已隐藏 + 公开列表仅剩 1', async ({ page }) => {
|
||
await loginAsAdmin(page)
|
||
const suffix = await createAlbumViaUi(page)
|
||
const albumId = await openAlbumDetail(page, suffix)
|
||
|
||
seedFixture('gov-photos', albumId, adminCredentials().account, '3')
|
||
await page.reload()
|
||
// UI06:照片默认网格视图;表格断言前先切换
|
||
await page.getByTestId('photo-view-table').click()
|
||
await expect(page.getByTestId('photo-list-table')).toBeVisible()
|
||
const rows = page.getByTestId('photo-list-table').locator('tbody tr')
|
||
await expect(rows).toHaveCount(3)
|
||
for (const row of await rows.all()) {
|
||
await expect(row.getByTestId('photo-visibility-tag')).toHaveText('已发布')
|
||
}
|
||
|
||
// 勾选前两行 → 批量按钮亮起(计数 2)→ 确认
|
||
await rows.nth(0).locator('.el-checkbox').click()
|
||
await rows.nth(1).locator('.el-checkbox').click()
|
||
const batchBtn = page.getByTestId('photo-batch-hide-btn')
|
||
await expect(batchBtn).toBeEnabled()
|
||
await expect(batchBtn).toContainText('2')
|
||
await batchBtn.click()
|
||
await page.getByRole('button', { name: '确定' }).click()
|
||
|
||
// 汇总 toast(全部成功分支)+ 两行原位已隐藏、一行保持已发布
|
||
await expect(page.locator('.el-message').filter({ hasText: '成功 2 张' })).toBeVisible()
|
||
await expect(rows.nth(0).getByTestId('photo-visibility-tag')).toHaveText('已隐藏')
|
||
await expect(rows.nth(1).getByTestId('photo-visibility-tag')).toHaveText('已隐藏')
|
||
await expect(rows.nth(2).getByTestId('photo-visibility-tag')).toHaveText('已发布')
|
||
|
||
// 观众侧:公开列表 3 → 1
|
||
const token = await page.getByTestId('album-detail-token').textContent()
|
||
expect(await publicPhotoCount(page, token ?? '')).toBe(1)
|
||
})
|
||
|
||
test('负责人交接:搜索 staff → 交接 → ownerId 变更(API + 详情回显)', async ({ page }) => {
|
||
await loginAsAdmin(page)
|
||
const suffix = await createAlbumViaUi(page)
|
||
const albumId = await openAlbumDetail(page, suffix)
|
||
|
||
const sessionToken = await apiLogin(page)
|
||
// 交接前 owner 是 admin 自己
|
||
const before = (await (
|
||
await page.request.get(`/v1/albums/${albumId}`, {
|
||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||
})
|
||
).json()) as { ownerId: string }
|
||
const staffId = (
|
||
(
|
||
(await (
|
||
await page.request.get('/v1/admin/users', {
|
||
params: { accountPrefix: 'staff-e2e' },
|
||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||
})
|
||
).json()) as { items: { id: string }[] }
|
||
).items[0] ?? { id: '' }
|
||
).id
|
||
expect(staffId).not.toBe('')
|
||
expect(before.ownerId).not.toBe(staffId)
|
||
|
||
// 交接对话框:前缀搜索(≥2 字符)→ 选中 → 保留协作者开关默认开 → 提交
|
||
await page.getByTestId('album-detail-transfer').click()
|
||
await expect(page.getByTestId('transfer-dialog')).toBeVisible()
|
||
const targetSelect = page.getByTestId('transfer-target-select')
|
||
await targetSelect.click()
|
||
await targetSelect.locator('input').fill('staff-e2e')
|
||
await page.getByTestId('transfer-option-staff-e2e').click()
|
||
await expect(page.getByTestId('transfer-keep-switch')).toHaveClass(/is-checked/)
|
||
await page.getByTestId('transfer-submit').click()
|
||
await expect(page.getByTestId('transfer-dialog')).toBeHidden()
|
||
await expect(page.locator('.el-message').filter({ hasText: '负责人已交接' })).toBeVisible()
|
||
|
||
// 详情回显 + API 权威断言
|
||
await expect(page.getByTestId('album-detail-owner-id')).toHaveText(staffId)
|
||
const after = (await (
|
||
await page.request.get(`/v1/albums/${albumId}`, {
|
||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||
})
|
||
).json()) as { ownerId: string }
|
||
expect(after.ownerId).toBe(staffId)
|
||
})
|
||
|
||
test('水印:配置保存 → 任务受理 → 直驱到已完成 → 公开 browse URL 指向水印图', async ({ page }) => {
|
||
await loginAsAdmin(page)
|
||
const suffix = await createAlbumViaUi(page)
|
||
const albumId = await openAlbumDetail(page, suffix)
|
||
const token = ((await page.getByTestId('album-detail-token').textContent()) ?? '').trim()
|
||
|
||
// 真实对象照片先行(任务 total=3,非空相册 autoDone 分支不触发)
|
||
seedFixture('wm-photos', albumId, adminCredentials().account, '3')
|
||
|
||
await page.getByTestId('album-watermark-tab').click()
|
||
await expect(page.getByTestId('watermark-panel')).toBeVisible()
|
||
await setSwitchState(page, 'watermark-enabled-switch', true)
|
||
await page.getByTestId('watermark-text-content').fill(`E2E水印 ${suffix}`)
|
||
|
||
// 保存 → 202 受理(v1)→ 任务行进入列表(pending/running)
|
||
await page.getByTestId('watermark-save-btn').click()
|
||
await expect(page.locator('.el-message').filter({ hasText: 'v1' })).toBeVisible()
|
||
const stateTag = page.getByTestId('watermark-jobs-table').locator('[data-state]')
|
||
await expect(stateTag.first()).toBeVisible()
|
||
await expect(stateTag.first()).toHaveText(/等待中|进行中/)
|
||
|
||
// e2e 无常驻 worker:fixture 直驱 worker dist 服务核心把任务跑到终态
|
||
// (页面进程独立,轮询不受 execSync 阻塞影响)
|
||
seedFixture('watermark-run', albumId)
|
||
|
||
// UI 3s 轮询翻到已完成;配置卡回显开启
|
||
await expect(stateTag.first()).toHaveText('已完成')
|
||
await expect(page.getByTestId('watermark-current-tag')).toBeVisible()
|
||
|
||
// 公开侧:album.watermarkEnabled + watermark 行存在 → browse 现签水印键
|
||
// (SignedUrlResponse 形状:{url, expiresAt})
|
||
const res = await page.request.get(`/v1/public/albums/${token}/photos`)
|
||
expect(res.ok()).toBeTruthy()
|
||
const body = (await res.json()) as { items: { browse: { url: string } }[] }
|
||
expect(body.items.length).toBe(3)
|
||
for (const item of body.items) {
|
||
expect(item.browse.url).toContain('/watermark.jpg')
|
||
}
|
||
})
|
||
|
||
test('用量:阈值调小触发告警 → 上调解除;合计与相册表可见', async ({ page }) => {
|
||
await loginAsAdmin(page)
|
||
|
||
// 独立数据:API 建相册 + 大字节照片(2MB > 0.00001GB 阈值),与 UI 顺序解耦
|
||
const sessionToken = await apiLogin(page)
|
||
const created = (await (
|
||
await page.request.post('/v1/albums', {
|
||
headers: { Authorization: `Bearer ${sessionToken}` },
|
||
data: {
|
||
defaultLocale: 'zh-CN',
|
||
startAt: '2026-10-01T10:00:00.000Z',
|
||
translations: [{ locale: 'zh-CN', title: `E2E用量相册 ${Date.now().toString(36)}` }]
|
||
}
|
||
})
|
||
).json()) as { id: string }
|
||
seedFixture('gov-photos', created.id, adminCredentials().account, '2', '2097152')
|
||
|
||
await page.goto('/#/system/usage')
|
||
await expect(page.getByTestId('usage-totals')).toBeVisible()
|
||
await expect(page.getByTestId('usage-albums-table')).toBeVisible()
|
||
// 合计已加载(种子照片计入,非空占位)
|
||
await expect(page.getByTestId('usage-total-photos')).not.toHaveText('-')
|
||
|
||
// 阈值调小(perAlbum 0.00001 GB ≈ 10KB < 2MB)→ 即刻评估出未解决告警。
|
||
// 开关状态感知:共享开发库可能残留上次运行的已存阈值(开关已开)——
|
||
// 幂等设到目标态而非盲目点击。
|
||
await page.getByTestId('usage-thresholds-btn').click()
|
||
await expect(page.getByTestId('thresholds-dialog')).toBeVisible()
|
||
await setSwitchState(page, 'thresholds-per-album-switch', true)
|
||
await page.getByTestId('thresholds-per-album-input').locator('input').fill('0.00001')
|
||
await page.keyboard.press('Enter')
|
||
await page.getByTestId('thresholds-submit').click()
|
||
await expect(page.getByTestId('thresholds-dialog')).toBeHidden()
|
||
|
||
await expect(
|
||
page.getByTestId('usage-alerts-table').locator('.alert-row-active').first()
|
||
).toBeVisible()
|
||
await expect(
|
||
page
|
||
.getByTestId('usage-alerts-table')
|
||
.locator('.alert-row-active')
|
||
.first()
|
||
.getByTestId('alert-state-tag')
|
||
).toHaveText('未解决')
|
||
|
||
// 上调到 100GB(storage 保持关闭=显式清除,避免历史阈值残留干扰)→ 解除
|
||
await page.getByTestId('usage-thresholds-btn').click()
|
||
await expect(page.getByTestId('thresholds-dialog')).toBeVisible()
|
||
await setSwitchState(page, 'thresholds-per-album-switch', true)
|
||
await setSwitchState(page, 'thresholds-storage-switch', false)
|
||
await page.getByTestId('thresholds-per-album-input').locator('input').fill('100')
|
||
await page.keyboard.press('Enter')
|
||
await page.getByTestId('thresholds-submit').click()
|
||
await expect(page.getByTestId('thresholds-dialog')).toBeHidden()
|
||
|
||
await expect(page.getByTestId('usage-alerts-table').locator('.alert-row-active')).toHaveCount(0)
|
||
await expect(
|
||
page.getByTestId('usage-alerts-table').locator('.alert-row-resolved').first()
|
||
).toBeVisible()
|
||
})
|
||
|
||
test('分享二维码:canvas 非空 + viewer URL + 复制链接', async ({ page, context }) => {
|
||
await loginAsAdmin(page)
|
||
const suffix = await createAlbumViaUi(page)
|
||
await openAlbumDetail(page, suffix)
|
||
const token = ((await page.getByTestId('album-detail-token').textContent()) ?? '').trim()
|
||
|
||
await page.getByTestId('album-share-tab').click()
|
||
await expect(page.getByTestId('share-panel')).toBeVisible()
|
||
|
||
// URL = VITE_VIEWER_BASE + /a/{token}
|
||
const urlText = ((await page.getByTestId('share-url').textContent()) ?? '').trim()
|
||
expect(urlText).toBe(`http://127.0.0.1:5175/a/${token}`)
|
||
|
||
// canvas 已绘制(空白 canvas 的 dataURL 约几十字节,绘制后远大于 100)
|
||
const dataUrlLength = await page
|
||
.getByTestId('share-qr-canvas')
|
||
.evaluate((canvas: HTMLCanvasElement) => canvas.toDataURL().length)
|
||
expect(dataUrlLength).toBeGreaterThan(100)
|
||
|
||
// 复制 → toast + 剪贴板内容一致(授 chromium 剪贴板权限)
|
||
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
|
||
await page.getByTestId('share-copy-btn').click()
|
||
await expect(page.locator('.el-message').filter({ hasText: '已复制' })).toBeVisible()
|
||
const clipped = await page.evaluate(() => navigator.clipboard.readText())
|
||
expect(clipped).toBe(urlText)
|
||
})
|