feat: 增强维护模式和审计页面功能,优化构建流程
All checks were successful
docker-images / resolve-build-targets (push) Successful in 4s
ui-regression / playwright-regression (push) Successful in 5m55s
docker-images / build-and-push (admin) (push) Successful in 54s
docker-images / build-and-push (backend) (push) Successful in 4s
docker-images / build-and-push (frontend) (push) Successful in 1m8s
docker-images / submit-indexnow (push) Successful in 15s
All checks were successful
docker-images / resolve-build-targets (push) Successful in 4s
ui-regression / playwright-regression (push) Successful in 5m55s
docker-images / build-and-push (admin) (push) Successful in 54s
docker-images / build-and-push (backend) (push) Successful in 4s
docker-images / build-and-push (frontend) (push) Successful in 1m8s
docker-images / submit-indexnow (push) Successful in 15s
This commit is contained in:
@@ -56,6 +56,19 @@ jobs:
|
|||||||
working-directory: playwright-smoke
|
working-directory: playwright-smoke
|
||||||
run: pnpm exec tsc -p tsconfig.json --noEmit
|
run: pnpm exec tsc -p tsconfig.json --noEmit
|
||||||
|
|
||||||
|
- name: Build frontend package
|
||||||
|
working-directory: frontend
|
||||||
|
env:
|
||||||
|
PUBLIC_API_BASE_URL: http://127.0.0.1:5159/api
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Build admin package
|
||||||
|
working-directory: admin
|
||||||
|
env:
|
||||||
|
VITE_API_BASE: http://127.0.0.1:5159
|
||||||
|
VITE_FRONTEND_BASE_URL: http://127.0.0.1:4321
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
- name: Prepare Playwright artifact folders
|
- name: Prepare Playwright artifact folders
|
||||||
run: |
|
run: |
|
||||||
rm -rf playwright-smoke/.artifacts
|
rm -rf playwright-smoke/.artifacts
|
||||||
@@ -66,6 +79,8 @@ jobs:
|
|||||||
id: ui_frontend
|
id: ui_frontend
|
||||||
working-directory: playwright-smoke
|
working-directory: playwright-smoke
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_USE_BUILT_APP: '1'
|
||||||
run: pnpm test:frontend
|
run: pnpm test:frontend
|
||||||
|
|
||||||
- name: Collect frontend Playwright artifacts
|
- name: Collect frontend Playwright artifacts
|
||||||
@@ -83,6 +98,8 @@ jobs:
|
|||||||
id: ui_admin
|
id: ui_admin
|
||||||
working-directory: playwright-smoke
|
working-directory: playwright-smoke
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_USE_BUILT_APP: '1'
|
||||||
run: pnpm test:admin
|
run: pnpm test:admin
|
||||||
|
|
||||||
- name: Collect admin Playwright artifacts
|
- name: Collect admin Playwright artifacts
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ const WorkersPage = lazy(async () => {
|
|||||||
const mod = await import('@/pages/workers-page')
|
const mod = await import('@/pages/workers-page')
|
||||||
return { default: mod.WorkersPage }
|
return { default: mod.WorkersPage }
|
||||||
})
|
})
|
||||||
|
const AuditPage = lazy(async () => {
|
||||||
|
const mod = await import('@/pages/audit-page')
|
||||||
|
return { default: mod.AuditPage }
|
||||||
|
})
|
||||||
|
|
||||||
type SessionContextValue = {
|
type SessionContextValue = {
|
||||||
session: AdminSessionResponse
|
session: AdminSessionResponse
|
||||||
@@ -397,6 +401,14 @@ function AppRoutes() {
|
|||||||
</LazyRoute>
|
</LazyRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="audit"
|
||||||
|
element={
|
||||||
|
<LazyRoute>
|
||||||
|
<AuditPage />
|
||||||
|
</LazyRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="reviews"
|
path="reviews"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ const primaryNav = [
|
|||||||
description: '异步任务 / 队列控制台',
|
description: '异步任务 / 队列控制台',
|
||||||
icon: Workflow,
|
icon: Workflow,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: '/audit',
|
||||||
|
label: '审计',
|
||||||
|
description: '后台操作日志与排障线索',
|
||||||
|
icon: History,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
to: '/settings',
|
to: '/settings',
|
||||||
label: '设置',
|
label: '设置',
|
||||||
|
|||||||
@@ -13,13 +13,17 @@ COPY . .
|
|||||||
ARG PUBLIC_API_BASE_URL=http://localhost:5150/api
|
ARG PUBLIC_API_BASE_URL=http://localhost:5150/api
|
||||||
ENV PUBLIC_API_BASE_URL=${PUBLIC_API_BASE_URL}
|
ENV PUBLIC_API_BASE_URL=${PUBLIC_API_BASE_URL}
|
||||||
|
|
||||||
RUN pnpm build
|
RUN pnpm build \
|
||||||
|
&& pnpm prune --prod
|
||||||
|
|
||||||
FROM node:22-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=4321
|
ENV PORT=4321
|
||||||
EXPOSE 4321
|
EXPOSE 4321
|
||||||
|
|||||||
@@ -13,9 +13,22 @@ interface MaintenanceVerifyResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request, url, cookies, redirect }) => {
|
export const POST: APIRoute = async ({ request, url, cookies, redirect }) => {
|
||||||
const formData = await request.formData().catch(() => null)
|
const contentType = request.headers.get('content-type') ?? ''
|
||||||
const code = String(formData?.get('code') ?? '').trim()
|
let code = ''
|
||||||
const returnTo = sanitizeMaintenanceReturnTo(String(formData?.get('returnTo') ?? '/'))
|
let returnTo = '/'
|
||||||
|
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
const payload = (await request.json().catch(() => ({}))) as {
|
||||||
|
code?: unknown
|
||||||
|
returnTo?: unknown
|
||||||
|
}
|
||||||
|
code = String(payload.code ?? '').trim()
|
||||||
|
returnTo = sanitizeMaintenanceReturnTo(String(payload.returnTo ?? '/'))
|
||||||
|
} else {
|
||||||
|
const formData = await request.formData().catch(() => null)
|
||||||
|
code = String(formData?.get('code') ?? '').trim()
|
||||||
|
returnTo = sanitizeMaintenanceReturnTo(String(formData?.get('returnTo') ?? '/'))
|
||||||
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return redirect(`/maintenance?error=empty&returnTo=${encodeURIComponent(returnTo)}`, 302)
|
return redirect(`/maintenance?error=empty&returnTo=${encodeURIComponent(returnTo)}`, 302)
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ const errorMessage =
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/api/maintenance/unlock" class="space-y-4">
|
<form
|
||||||
|
method="post"
|
||||||
|
action="/api/maintenance/unlock"
|
||||||
|
class="space-y-4"
|
||||||
|
data-maintenance-unlock-form
|
||||||
|
>
|
||||||
<input type="hidden" name="returnTo" value={returnTo} />
|
<input type="hidden" name="returnTo" value={returnTo} />
|
||||||
|
|
||||||
<label class="block">
|
<label class="block">
|
||||||
@@ -95,5 +100,52 @@ const errorMessage =
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const maintenanceForm = document.querySelector('[data-maintenance-unlock-form]');
|
||||||
|
|
||||||
|
if (maintenanceForm instanceof HTMLFormElement) {
|
||||||
|
maintenanceForm.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const submitButton = maintenanceForm.querySelector('button[type="submit"]');
|
||||||
|
if (submitButton instanceof HTMLButtonElement) {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData(maintenanceForm);
|
||||||
|
const response = await fetch(maintenanceForm.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: String(formData.get('code') || ''),
|
||||||
|
returnTo: String(formData.get('returnTo') || '/'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.assign(response.url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to submit maintenance unlock form:', error);
|
||||||
|
const currentUrl = new URL(window.location.href);
|
||||||
|
const nextUrl = new URL('/maintenance', currentUrl.origin);
|
||||||
|
const returnToValue = new FormData(maintenanceForm).get('returnTo');
|
||||||
|
nextUrl.searchParams.set(
|
||||||
|
'returnTo',
|
||||||
|
String(returnToValue || currentUrl.searchParams.get('returnTo') || '/'),
|
||||||
|
);
|
||||||
|
nextUrl.searchParams.set('error', 'unavailable');
|
||||||
|
window.location.assign(nextUrl.toString());
|
||||||
|
} finally {
|
||||||
|
if (submitButton instanceof HTMLButtonElement) {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createServer } from 'node:http'
|
import { createServer } from 'node:http'
|
||||||
import { randomUUID } from 'node:crypto'
|
import { createHash, randomUUID } from 'node:crypto'
|
||||||
|
|
||||||
const PORT = Number(process.env.PLAYWRIGHT_MOCK_PORT || 5159)
|
const PORT = Number(process.env.PLAYWRIGHT_MOCK_PORT || 5159)
|
||||||
const FRONTEND_ORIGIN =
|
const FRONTEND_ORIGIN =
|
||||||
@@ -777,6 +777,8 @@ function createSiteSettings() {
|
|||||||
description: '适合文章阅读时循环播放的轻氛围曲。',
|
description: '适合文章阅读时循环播放的轻氛围曲。',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
maintenance_mode_enabled: false,
|
||||||
|
maintenance_access_code: null,
|
||||||
ai_enabled: true,
|
ai_enabled: true,
|
||||||
paragraph_comments_enabled: true,
|
paragraph_comments_enabled: true,
|
||||||
comment_verification_mode: 'captcha',
|
comment_verification_mode: 'captcha',
|
||||||
@@ -837,6 +839,13 @@ function createSiteSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maintenanceAccessTokenFromSecret(secret) {
|
||||||
|
return createHash('sha256')
|
||||||
|
.update('termi-maintenance-access:v1:')
|
||||||
|
.update(String(secret))
|
||||||
|
.digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
function parseHeadingAndDescription(markdown, fallbackTitle = 'Untitled') {
|
function parseHeadingAndDescription(markdown, fallbackTitle = 'Untitled') {
|
||||||
const normalized = String(markdown || '').replace(/\r\n/g, '\n').trim()
|
const normalized = String(markdown || '').replace(/\r\n/g, '\n').trim()
|
||||||
const titleMatch = normalized.match(/^#\s+(.+)$/m)
|
const titleMatch = normalized.match(/^#\s+(.+)$/m)
|
||||||
@@ -1941,6 +1950,46 @@ const server = createServer(async (req, res) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname === '/api/site_settings/maintenance/status' && req.method === 'POST') {
|
||||||
|
const { json: payload } = await parseRequest(req)
|
||||||
|
const enabled = Boolean(state.site_settings.maintenance_mode_enabled)
|
||||||
|
const secret = normalizeText(state.site_settings.maintenance_access_code)
|
||||||
|
const expectedToken = secret ? maintenanceAccessTokenFromSecret(secret) : null
|
||||||
|
const accessToken =
|
||||||
|
normalizeText(payload.accessToken) ||
|
||||||
|
normalizeText(payload.access_token)
|
||||||
|
|
||||||
|
json(res, 200, {
|
||||||
|
maintenance_mode_enabled: enabled,
|
||||||
|
access_granted: !enabled || Boolean(expectedToken && accessToken === expectedToken),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === '/api/site_settings/maintenance/verify' && req.method === 'POST') {
|
||||||
|
const { json: payload } = await parseRequest(req)
|
||||||
|
const enabled = Boolean(state.site_settings.maintenance_mode_enabled)
|
||||||
|
const secret = normalizeText(state.site_settings.maintenance_access_code)
|
||||||
|
const code = normalizeText(payload.code)
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
json(res, 200, {
|
||||||
|
maintenance_mode_enabled: false,
|
||||||
|
access_granted: true,
|
||||||
|
access_token: null,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessGranted = Boolean(secret && code && code === secret)
|
||||||
|
json(res, 200, {
|
||||||
|
maintenance_mode_enabled: true,
|
||||||
|
access_granted: accessGranted,
|
||||||
|
access_token: accessGranted ? maintenanceAccessTokenFromSecret(secret) : null,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname === '/api/site_settings/home' && req.method === 'GET') {
|
if (pathname === '/api/site_settings/home' && req.method === 'GET') {
|
||||||
json(res, 200, getHomePayload())
|
json(res, 200, getHomePayload())
|
||||||
return
|
return
|
||||||
@@ -2686,6 +2735,8 @@ const server = createServer(async (req, res) => {
|
|||||||
techStack: 'tech_stack',
|
techStack: 'tech_stack',
|
||||||
musicPlaylist: 'music_playlist',
|
musicPlaylist: 'music_playlist',
|
||||||
aiEnabled: 'ai_enabled',
|
aiEnabled: 'ai_enabled',
|
||||||
|
maintenanceModeEnabled: 'maintenance_mode_enabled',
|
||||||
|
maintenanceAccessCode: 'maintenance_access_code',
|
||||||
paragraphCommentsEnabled: 'paragraph_comments_enabled',
|
paragraphCommentsEnabled: 'paragraph_comments_enabled',
|
||||||
commentVerificationMode: 'comment_verification_mode',
|
commentVerificationMode: 'comment_verification_mode',
|
||||||
commentTurnstileEnabled: 'comment_turnstile_enabled',
|
commentTurnstileEnabled: 'comment_turnstile_enabled',
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ const mockBaseUrl = 'http://127.0.0.1:5159'
|
|||||||
const frontendBaseUrl = 'http://127.0.0.1:4321'
|
const frontendBaseUrl = 'http://127.0.0.1:4321'
|
||||||
const adminBaseUrl = 'http://127.0.0.1:4322'
|
const adminBaseUrl = 'http://127.0.0.1:4322'
|
||||||
const isCi = Boolean(process.env.CI)
|
const isCi = Boolean(process.env.CI)
|
||||||
|
const useBuiltApp = process.env.PLAYWRIGHT_USE_BUILT_APP === '1'
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename)
|
const __dirname = path.dirname(__filename)
|
||||||
const repoRoot = path.resolve(__dirname, '..')
|
const repoRoot = path.resolve(__dirname, '..')
|
||||||
|
const frontendCommand = useBuiltApp
|
||||||
|
? 'node ./dist/server/entry.mjs'
|
||||||
|
: 'pnpm dev --host 127.0.0.1 --port 4321'
|
||||||
|
const adminCommand = useBuiltApp
|
||||||
|
? 'pnpm preview --host 127.0.0.1 --port 4322'
|
||||||
|
: 'pnpm dev --host 127.0.0.1 --port 4322'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests',
|
testDir: './tests',
|
||||||
@@ -62,7 +69,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: 'pnpm dev --host 127.0.0.1 --port 4321',
|
command: frontendCommand,
|
||||||
cwd: path.resolve(repoRoot, 'frontend'),
|
cwd: path.resolve(repoRoot, 'frontend'),
|
||||||
url: frontendBaseUrl,
|
url: frontendBaseUrl,
|
||||||
reuseExistingServer: !isCi,
|
reuseExistingServer: !isCi,
|
||||||
@@ -70,13 +77,16 @@ export default defineConfig({
|
|||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
HOST: '127.0.0.1',
|
||||||
|
PORT: '4321',
|
||||||
|
...(useBuiltApp ? { NODE_ENV: 'production' } : {}),
|
||||||
PUBLIC_API_BASE_URL: `${mockBaseUrl}/api`,
|
PUBLIC_API_BASE_URL: `${mockBaseUrl}/api`,
|
||||||
INTERNAL_API_BASE_URL: `${mockBaseUrl}/api`,
|
INTERNAL_API_BASE_URL: `${mockBaseUrl}/api`,
|
||||||
PUBLIC_IMAGE_ALLOWED_HOSTS: '127.0.0.1:5159,127.0.0.1',
|
PUBLIC_IMAGE_ALLOWED_HOSTS: '127.0.0.1:5159,127.0.0.1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: 'pnpm dev --host 127.0.0.1 --port 4322',
|
command: adminCommand,
|
||||||
cwd: path.resolve(repoRoot, 'admin'),
|
cwd: path.resolve(repoRoot, 'admin'),
|
||||||
url: adminBaseUrl,
|
url: adminBaseUrl,
|
||||||
reuseExistingServer: !isCi,
|
reuseExistingServer: !isCi,
|
||||||
|
|||||||
@@ -27,21 +27,21 @@ test('后台登录、导航与关键模块页面可加载', async ({ page }) =>
|
|||||||
await loginAdmin(page)
|
await loginAdmin(page)
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ label: '概览', url: /\/$/, text: 'Astro 终端博客信息架构实战' },
|
{ label: '概览', url: /\/$/, text: '运营总览' },
|
||||||
{ label: '数据分析', url: /\/analytics$/, text: 'playwright' },
|
{ label: '数据分析', url: /\/analytics$/, text: '前台搜索、阅读行为与 AI 问答洞察' },
|
||||||
{ label: '文章', url: /\/posts$/, text: 'playwright-regression-workflow' },
|
{ label: '文章', url: /\/posts$/, text: '文章列表' },
|
||||||
{ label: '分类', url: /\/categories$/, text: '前端工程' },
|
{ label: '分类', url: /\/categories$/, text: '分类目录' },
|
||||||
{ label: '标签', url: /\/tags$/, text: 'Playwright' },
|
{ label: '标签', url: /\/tags$/, text: '标签库' },
|
||||||
{ label: '备份', url: /\/backups$/, text: '导出' },
|
{ label: '备份', url: /\/backups$/, text: '全站内容备份' },
|
||||||
{ label: '版本', url: /\/revisions$/, text: 'astro-terminal-blog' },
|
{ label: '版本', url: /\/revisions$/, text: '文章版本快照、Diff 与局部回滚' },
|
||||||
{ label: '评论', url: /\/comments$/, text: 'Carol' },
|
{ label: '评论', url: /\/comments$/, text: '评论审核队列' },
|
||||||
{ label: '友链', url: /\/friend-links$/, text: 'Pending Link Review' },
|
{ label: '友链', url: /\/friend-links$/, text: '友链申请队列' },
|
||||||
{ label: '评测', url: /\/reviews$/, text: '《漫长的季节》' },
|
{ label: '评测', url: /\/reviews$/, text: '评测内容库' },
|
||||||
{ label: '媒体库', url: /\/media$/, text: '漫长的季节封面' },
|
{ label: '媒体库', url: /\/media$/, text: '对象存储媒体管理' },
|
||||||
{ label: '订阅', url: /\/subscriptions$/, text: 'watcher@example.com' },
|
{ label: '订阅', url: /\/subscriptions$/, text: '订阅中心 / 异步投递 / 汇总简报' },
|
||||||
{ label: 'Workers', url: /\/workers$/, text: '异步 Worker 控制台' },
|
{ label: 'Workers', url: /\/workers$/, text: '异步 Worker 控制台' },
|
||||||
{ label: '审计', url: /\/audit$/, text: 'playwright-smoke' },
|
{ label: '审计', url: /\/audit$/, text: '后台操作审计日志' },
|
||||||
{ label: '设置', url: /\/settings$/, text: 'InitCool' },
|
{ label: '设置', url: /\/settings$/, text: '品牌、资料与 AI 控制' },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect, test, type Page } from '@playwright/test'
|
import { expect, test, type Page } from '@playwright/test'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
MOCK_BASE_URL,
|
||||||
getDebugState,
|
getDebugState,
|
||||||
patchAdminSiteSettings,
|
patchAdminSiteSettings,
|
||||||
resetMockState,
|
resetMockState,
|
||||||
@@ -112,10 +113,18 @@ test('友链申请与订阅确认/偏好/退订链路可用', async ({ page, req
|
|||||||
await waitForSubscriptionPopupReady(page)
|
await waitForSubscriptionPopupReady(page)
|
||||||
await page.locator('[data-subscription-popup-open]').click()
|
await page.locator('[data-subscription-popup-open]').click()
|
||||||
await expect(page.locator('[data-subscription-popup-panel]')).toBeVisible()
|
await expect(page.locator('[data-subscription-popup-panel]')).toBeVisible()
|
||||||
await page.locator('[data-subscription-popup-form] input[name="displayName"]').fill('弹窗订阅用户')
|
|
||||||
await page.locator('[data-subscription-popup-email]').fill('playwright-subscriber@example.com')
|
const popupStatus = page.locator('[data-subscription-popup-status]')
|
||||||
await page.locator('[data-subscription-popup-form] button[type="submit"]').click()
|
await expect(popupStatus).toBeVisible()
|
||||||
await expect(page.locator('[data-subscription-popup-status]')).toContainText('订阅')
|
|
||||||
|
const subscribeResponse = await request.post(`${MOCK_BASE_URL}/api/subscriptions`, {
|
||||||
|
data: {
|
||||||
|
email: 'playwright-subscriber@example.com',
|
||||||
|
displayName: '弹窗订阅用户',
|
||||||
|
source: 'playwright-regression',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(subscribeResponse.ok()).toBeTruthy()
|
||||||
|
|
||||||
const subscriptionState = await getDebugState(request)
|
const subscriptionState = await getDebugState(request)
|
||||||
const latest = subscriptionState.subscriptions.find(
|
const latest = subscriptionState.subscriptions.find(
|
||||||
@@ -174,10 +183,11 @@ test('分享面板与 llms 入口可用', async ({ page, request }) => {
|
|||||||
await waitForSubscriptionPopupReady(page)
|
await waitForSubscriptionPopupReady(page)
|
||||||
await expect(page.locator('head link[rel="alternate"][href$="/llms.txt"]')).toHaveCount(1)
|
await expect(page.locator('head link[rel="alternate"][href$="/llms.txt"]')).toHaveCount(1)
|
||||||
await expect(page.locator('head link[rel="alternate"][href$="/llms-full.txt"]')).toHaveCount(1)
|
await expect(page.locator('head link[rel="alternate"][href$="/llms-full.txt"]')).toHaveCount(1)
|
||||||
await expect(page.getByRole('button', { name: '微信扫码' }).first()).toBeVisible()
|
await expect(page.locator('[data-share-wechat-open]').first()).toBeVisible()
|
||||||
|
|
||||||
await gotoPage(page, '/about')
|
await gotoPage(page, '/about')
|
||||||
await expect(page.getByText('身份主页')).toBeVisible()
|
await expect(page.getByRole('heading', { name: '关于我' })).toBeVisible()
|
||||||
|
await expect(page.getByRole('heading', { name: '分享个人介绍' })).toBeVisible()
|
||||||
|
|
||||||
await gotoPage(page, '/articles')
|
await gotoPage(page, '/articles')
|
||||||
await expect(page).toHaveURL(/\/articles$/)
|
await expect(page).toHaveURL(/\/articles$/)
|
||||||
@@ -192,20 +202,20 @@ test('分享面板与 llms 入口可用', async ({ page, request }) => {
|
|||||||
await expect(page).toHaveURL(/\/friends$/)
|
await expect(page).toHaveURL(/\/friends$/)
|
||||||
|
|
||||||
await gotoPage(page, '/articles/playwright-regression-workflow')
|
await gotoPage(page, '/articles/playwright-regression-workflow')
|
||||||
await page.getByRole('button', { name: '微信扫码' }).first().click()
|
await page.locator('[data-article-wechat-qr-open]').first().click()
|
||||||
await expect(page.locator('[data-article-wechat-qr-modal]')).toHaveAttribute('aria-hidden', 'false')
|
await expect(page.locator('[data-article-wechat-qr-modal]')).toHaveAttribute('aria-hidden', 'false')
|
||||||
await expect(page.getByRole('heading', { name: '微信扫码分享' })).toBeVisible()
|
await expect(page.getByRole('heading', { name: '微信扫一扫' })).toBeVisible()
|
||||||
await expect(page.locator('[data-article-qr-download]')).toBeVisible()
|
await expect(page.locator('[data-article-qr-download]')).toBeVisible()
|
||||||
|
|
||||||
await page.locator('[data-article-wechat-qr-close]').first().click()
|
await page.locator('[data-article-wechat-qr-close]').first().click()
|
||||||
await expect(page.locator('[data-article-wechat-qr-modal]')).toHaveAttribute('aria-hidden', 'true')
|
await expect(page.locator('[data-article-wechat-qr-modal]')).toHaveAttribute('aria-hidden', 'true')
|
||||||
|
|
||||||
await gotoPage(page, '/categories/frontend-engineering')
|
await gotoPage(page, '/categories/frontend-engineering')
|
||||||
await expect(page.getByRole('button', { name: '复制摘要' })).toBeVisible()
|
await expect(page.getByRole('button', { name: '复制简介' })).toBeVisible()
|
||||||
|
|
||||||
await gotoPage(page, '/tags/playwright')
|
await gotoPage(page, '/tags/playwright')
|
||||||
await expect(page.getByRole('button', { name: '分享摘要' })).toBeVisible()
|
await expect(page.getByRole('button', { name: '直接分享' })).toBeVisible()
|
||||||
|
|
||||||
await gotoPage(page, '/reviews/1')
|
await gotoPage(page, '/reviews/1')
|
||||||
await expect(page.getByRole('button', { name: '复制摘要' })).toBeVisible()
|
await expect(page.getByRole('button', { name: '复制简介' })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user