feat: add worker operations and fix gitea actions
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 29s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Successful in 33m13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 58s
ui-regression / playwright-regression (push) Failing after 13m24s

This commit is contained in:
2026-04-02 03:43:37 +08:00
parent ee0bec4a78
commit a516be2e91
37 changed files with 3890 additions and 879 deletions

View File

@@ -1,6 +1,6 @@
import { expect, test, type Page } from '@playwright/test'
import { getDebugState, loginAdmin, resetMockState } from './helpers'
import { getDebugState, loginAdmin, MOCK_BASE_URL, resetMockState } from './helpers'
test.beforeEach(async ({ request }) => {
await resetMockState(request)
@@ -39,6 +39,7 @@ test('后台登录、导航与关键模块页面可加载', async ({ page }) =>
{ label: '评测', url: /\/reviews$/, text: '《漫长的季节》' },
{ label: '媒体库', url: /\/media$/, text: '漫长的季节封面' },
{ label: '订阅', url: /\/subscriptions$/, text: 'watcher@example.com' },
{ label: 'Workers', url: /\/workers$/, text: '异步 Worker 控制台' },
{ label: '审计', url: /\/audit$/, text: 'playwright-smoke' },
{ label: '设置', url: /\/settings$/, text: 'InitCool' },
]
@@ -50,6 +51,17 @@ test('后台登录、导航与关键模块页面可加载', async ({ page }) =>
}
})
test('后台 dashboard worker 健康卡片可跳转到带筛选的 workers', async ({ page }) => {
await loginAdmin(page)
await expect(page.locator('main')).toContainText('Worker 活动')
await page.getByTestId('dashboard-worker-card-failed').click()
await expect(page).toHaveURL(/\/workers\?status=failed$/)
await expect(page.locator('main')).toContainText('异步 Worker 控制台')
await expect(page.locator('main')).toContainText('failed')
})
test('后台可以审核评论和友链,并更新站点设置', async ({ page }) => {
await loginAdmin(page)
@@ -136,8 +148,21 @@ test('后台可完成订阅 CRUD、测试投递与 digest 入队', async ({ page
await expect(row).toContainText('Deep Regression Updated')
await row.getByTestId(/subscription-test-/).click()
await expect(page.getByTestId('subscriptions-last-job')).toBeVisible()
await page.getByTestId('subscriptions-last-job').click()
await expect(page).toHaveURL(/\/workers\?job=\d+$/)
await expect(page.locator('main')).toContainText('subscription.test')
await page.getByRole('link', { name: '订阅' }).click()
await page.getByTestId('subscriptions-send-weekly').click()
await expect(page.getByTestId('subscriptions-last-job')).toBeVisible()
await page.getByTestId('subscriptions-send-monthly').click()
await expect(page.getByTestId(/^subscription-delivery-job-/).first()).toBeVisible()
await page.getByTestId(/^subscription-delivery-job-/).first().click()
await expect(page).toHaveURL(/\/workers\?job=\d+$/)
await expect(page.locator('main')).toContainText('worker.notification_delivery')
await page.getByRole('link', { name: '订阅' }).click()
let state = await getDebugState(request)
expect(
@@ -146,6 +171,9 @@ test('后台可完成订阅 CRUD、测试投递与 digest 入队', async ({ page
).toBeTruthy()
expect(state.deliveries.some((item: { event_type: string }) => item.event_type === 'digest.weekly')).toBeTruthy()
expect(state.deliveries.some((item: { event_type: string }) => item.event_type === 'digest.monthly')).toBeTruthy()
expect(
state.worker_jobs.some((item: { worker_name: string }) => item.worker_name === 'worker.notification_delivery'),
).toBeTruthy()
await row.getByTestId(/subscription-delete-/).click()
await expect(row).toHaveCount(0)
@@ -156,6 +184,33 @@ test('后台可完成订阅 CRUD、测试投递与 digest 入队', async ({ page
).toBeFalsy()
})
test('后台可查看 worker 控制台并执行 digest / retry / job 重跑', async ({ page, request }) => {
await loginAdmin(page)
await page.getByRole('link', { name: 'Workers' }).click()
await page.getByTestId('workers-run-weekly').click()
await expect(page.locator('main')).toContainText('task.send_weekly_digest')
await page.getByTestId('workers-retry-job').click()
let state = await getDebugState(request)
expect(
state.worker_jobs.some((item: { worker_name: string }) => item.worker_name === 'task.send_weekly_digest'),
).toBeTruthy()
expect(
state.worker_jobs.some(
(item: { worker_name: string; parent_job_id: number | null }) =>
item.worker_name === 'task.send_weekly_digest' && item.parent_job_id !== null,
),
).toBeTruthy()
await page.getByTestId('workers-run-retry').click()
state = await getDebugState(request)
expect(
state.worker_jobs.some((item: { worker_name: string }) => item.worker_name === 'task.retry_deliveries'),
).toBeTruthy()
})
test('后台可完成文章创建、保存、版本恢复与删除', async ({ page, request }) => {
await loginAdmin(page)
await page.getByRole('link', { name: '文章' }).click()
@@ -209,6 +264,15 @@ test('后台可完成媒体库上传/元数据/替换/删除,并执行设置
await loginAdmin(page)
await page.getByRole('link', { name: '媒体库' }).click()
await page.getByTestId('media-remote-url').fill(`${MOCK_BASE_URL}/media-files/remote-playwright.svg`)
await page.getByTestId('media-remote-title').fill('Remote Playwright Cover')
await page.getByTestId('media-remote-download').click()
await expect(page.getByTestId('media-last-remote-job')).toBeVisible()
await page.getByTestId('media-last-remote-job').click()
await expect(page).toHaveURL(/\/workers\?job=\d+$/)
await expect(page.locator('main')).toContainText('worker.download_media')
await page.getByRole('link', { name: '媒体库' }).click()
await page.getByTestId('media-upload-input').setInputFiles([
buildSvgPayload('deep-regression-cover.svg', 'deep-upload'),
])
@@ -222,6 +286,13 @@ test('后台可完成媒体库上传/元数据/替换/删除,并执行设置
await page.getByTestId('media-save-metadata').click()
let state = await getDebugState(request)
expect(
state.media.some(
(item: { title: string; key: string }) =>
item.title === 'Remote Playwright Cover' &&
String(item.key || '').includes('post-covers/'),
),
).toBeTruthy()
expect(
state.media.some(
(item: { title: string; alt_text: string; tags: string[] }) =>

View File

@@ -24,6 +24,36 @@ test('首页过滤、热门区和文章详情链路可用', async ({ page }) =>
await expect(page).toHaveURL(/\/articles\/playwright-regression-workflow$/)
await expect(page.getByRole('heading', { name: 'Playwright 回归工作流设计' })).toBeVisible()
await expect(page.locator('.paragraph-comment-marker').first()).toBeVisible()
await page.goto('/categories/frontend-engineering')
await expect(page.getByRole('heading', { name: '前端工程' })).toBeVisible()
await expect(page.getByText('Astro 终端博客信息架构实战')).toBeVisible()
await page.goto('/tags/playwright')
await expect(page.getByRole('heading', { name: 'Playwright', exact: true })).toBeVisible()
await expect(page.getByText('Playwright 回归工作流设计')).toBeVisible()
await page.goto('/reviews')
await expect(page.getByText('《宇宙探索编辑部》')).toHaveCount(0)
await page.goto('/reviews/4')
await expect(page.getByRole('heading', { name: '评价不存在' })).toBeVisible()
await page.goto('/reviews/1')
await page.getByRole('link', { name: '#年度最佳' }).click()
await expect(page).toHaveURL(/\/reviews\?tag=%E5%B9%B4%E5%BA%A6%E6%9C%80%E4%BD%B3$/)
await expect(page.getByText('《漫长的季节》')).toBeVisible()
await page.goto('/reviews/1')
await page.getByRole('link', { name: '动画' }).click()
await expect(page).toHaveURL(/\/reviews\?type=anime$/)
await expect(page.locator('#reviews-subtitle')).toContainText('动画')
await expect(page.getByText('《漫长的季节》')).toBeVisible()
await page.goto('/reviews/1')
await page.getByRole('link', { name: '已完成' }).click()
await expect(page).toHaveURL(/\/reviews\?status=completed$/)
await expect(page.locator('#reviews-subtitle')).toContainText('已完成')
await expect(page.getByText('《漫长的季节》')).toBeVisible()
})
test('文章评论、搜索和 AI 问答链路可用', async ({ page, request }) => {
@@ -64,16 +94,27 @@ test('友链申请与订阅确认/偏好/退订链路可用', async ({ page, req
expect(friendState.friend_links.some((item: { site_name: string }) => item.site_name === 'Playwright Friend')).toBeTruthy()
await page.goto('/')
await page.locator('[data-subscribe-form] input[name="displayName"]').fill('首页订阅用户')
await page.locator('[data-subscribe-form] input[name="email"]').fill('inline-subscriber@example.com')
await page.locator('[data-subscribe-form] button[type="submit"]').click()
await expect(page.locator('[data-subscribe-status]')).toContainText('订阅')
await page.locator('[data-subscription-popup-open]').click()
await page.locator('[data-subscription-popup-form] input[name="displayName"]').fill('弹窗订阅用户')
await page.locator('[data-subscription-popup-email]').fill('playwright-subscriber@example.com')
await page.locator('[data-subscription-popup-form] button[type="submit"]').click()
await expect(page.locator('[data-subscription-popup-status]')).toContainText('订阅')
const subscriptionState = await getDebugState(request)
const inlineRecord = subscriptionState.subscriptions.find(
(item: { target: string; display_name: string }) => item.target === 'inline-subscriber@example.com',
)
expect(inlineRecord?.display_name).toBe('首页订阅用户')
const latest = subscriptionState.subscriptions.find(
(item: { target: string }) => item.target === 'playwright-subscriber@example.com',
(item: { target: string; display_name: string }) => item.target === 'playwright-subscriber@example.com',
)
expect(latest).toBeTruthy()
expect(latest.display_name).toBe('弹窗订阅用户')
await page.goto(`/subscriptions/confirm?token=${encodeURIComponent(latest.confirm_token)}`)
await expect(page.getByText('订阅已确认')).toBeVisible()