import { test, expect, type Page, type APIRequestContext } from '@playwright/test'; /** * UJ: 用户旅程 E2E 测试 * * 覆盖范围: * UJ-01: 潜在客户从首页到联系表单的完整旅程 * UJ-02: 行业客户浏览解决方案到产品的旅程 * UJ-03: 内容管理员完整旅程(登录 → 仪表盘 → 内容管理 → 编辑 → 前台可见) * UJ-04: 新闻读者浏览旅程(列表 → 详情 → 返回) * UJ-05: 案例浏览者筛选旅程(列表 → 筛选 → 详情) * UJ-06: 多角色管理员权限旅程(超级管理员 → 角色 → 权限 → 用户) * UJ-07: 媒体管理员上传旅程 * UJ-10: 深度搜索者旅程(分类浏览 → 逐篇阅读 → 内容发现) */ test.setTimeout(90000); // ==================== 辅助函数 ==================== /** * 关闭 Cookie 同意横幅(如果存在) */ async function closeCookieBanner(page: Page) { const acceptAllButton = page.locator('button:has-text("接受所有")').first(); if (await acceptAllButton.count() > 0 && await acceptAllButton.isVisible()) { await acceptAllButton.click(); await page.waitForTimeout(500); } } /** * 从桌面主导航的产品下拉菜单中导航到指定链接 */ async function navigateFromProductDropdown(page: Page, linkText: string) { const desktopNav = page.locator('[data-testid="desktop-navigation"]').first(); const productDropdown = desktopNav.locator('button:has-text("产品")').first(); await expect(productDropdown).toBeVisible({ timeout: 5000 }); await productDropdown.hover(); await page.waitForTimeout(300); const targetLink = desktopNav.locator(`a:has-text("${linkText}")`).first(); await expect(targetLink).toBeVisible({ timeout: 5000 }); // 鼠标悬停在链接上,防止下拉菜单因 mouseleave 关闭 await targetLink.hover(); await page.waitForTimeout(200); await targetLink.click(); } // ==================== UJ-01: 潜在客户从首页到联系表单的完整旅程 ==================== test.describe('UJ-01: 潜在客户从首页到联系表单的完整旅程', () => { test('@journey @regression 访客从首页浏览产品并提交联系表单', async ({ page }) => { // === Step 1: 访问首页,验证 Hero 区域 === await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2500); await closeCookieBanner(page); // 验证 Hero 区域可见 const heroTitle = page.locator('h1').first(); await expect(heroTitle).toBeVisible({ timeout: 15000 }); const titleText = await heroTitle.textContent(); expect(titleText).toBeTruthy(); expect(titleText!.length).toBeGreaterThan(0); // 验证 Hero 描述段落可见 const heroDescription = page.locator('main p').first(); await expect(heroDescription).toBeVisible(); // === Step 2: 通过导航下拉菜单进入产品页面 === await navigateFromProductDropdown(page, '查看全部产品'); // === Step 3: 验证产品页面加载成功 === await page.waitForURL(/\/products/, { timeout: 10000 }); await expect(page).toHaveURL(/\/products/); await page.waitForTimeout(2000); // 验证产品页面有标题 const productsTitle = page.locator('h1, h2').first(); await expect(productsTitle).toBeVisible(); const productsTitleText = await productsTitle.textContent(); expect(productsTitleText).toContain('产品'); // 验证产品卡片存在 const productLinks = page.locator('a[href*="/products/"]:not([href$="/products"])'); const linkCount = await productLinks.count(); expect(linkCount).toBeGreaterThan(0); // === Step 4: 点击产品进入详情页 === const firstProductLink = productLinks.first(); await firstProductLink.click(); await page.waitForURL(/\/products\/.+/, { timeout: 10000 }); await expect(page).toHaveURL(/\/products\/.+/); await page.waitForTimeout(2000); // === Step 5: 验证产品详情页的四层叙事结构 === // L1 Hero: 产品标题可见 const detailTitle = page.locator('h1').first(); await expect(detailTitle).toBeVisible({ timeout: 10000 }); const detailTitleText = await detailTitle.textContent(); expect(detailTitleText).toBeTruthy(); expect(detailTitleText!.length).toBeGreaterThan(0); // L2 Value: 产品详情页应包含核心功能或产品价值区域 const mainContent = page.locator('main').first(); await expect(mainContent).toBeVisible(); const mainText = await mainContent.textContent(); expect(mainText!.length).toBeGreaterThan(200); // 验证功能/价值相关内容存在(核心功能或产品价值标签) const valueIndicators = ['核心功能', '产品价值', '实施流程']; let hasValueSection = false; for (const indicator of valueIndicators) { if (mainText!.includes(indicator)) { hasValueSection = true; break; } } expect(hasValueSection).toBe(true); // L3 Trust: 验证信任相关内容(客户案例或资质认证) const trustIndicators = ['客户案例', '资质认证', '可衡量的成果']; let hasTrustSection = false; for (const indicator of trustIndicators) { if (mainText!.includes(indicator)) { hasTrustSection = true; break; } } expect(hasTrustSection).toBe(true); // L4 CTA: 验证 CTA 区域存在(包含"预约演示"或"开始使用"链接) const ctaLink = page.locator('a[href="/contact"]').first(); await expect(ctaLink).toBeVisible(); // === Step 6: 导航到联系页面 === await page.goto('/contact', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2500); // === Step 7: 填写联系表单 === // 验证表单字段可见 const nameInput = page.locator('[data-testid="name-input"]').first(); await expect(nameInput).toBeVisible({ timeout: 5000 }); await page.locator('[data-testid="name-input"]').fill('潜在客户'); await page.locator('[data-testid="phone-input"]').fill('13912345678'); await page.locator('[data-testid="email-input"]').fill('prospect@example.com'); await page.locator('[data-testid="subject-input"]').fill('产品咨询 - ERP 管理系统'); await page.locator('[data-testid="message-input"]').fill('您好,我在浏览了贵司的 ERP 产品后非常感兴趣,希望进一步了解产品功能和实施细节,请安排专人联系。'); // === Step 8: 拦截 API 并提交表单 === let routeHit = false; await page.route('/api/contact', async (route) => { routeHit = true; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, message: '发送成功' }), }); }); const submitButton = page.locator('[data-testid="submit-button"]').first(); await submitButton.scrollIntoViewIfNeeded(); await page.waitForTimeout(300); // 隐藏可能覆盖底部元素的固定返回顶部按钮 await page.evaluate(() => { const backToTop = document.querySelector('[aria-label="返回顶部"]') as HTMLElement | null; if (backToTop) backToTop.style.display = 'none'; }); // 使用原生 DOM click() 触发提交 await page.evaluate(() => { const btn = document.querySelector('[data-testid="submit-button"]') as HTMLButtonElement | null; btn?.click(); }); // === Step 9: 验证成功消息 === await page.waitForTimeout(2000); console.log('UJ-01 route hit:', routeHit); // 验证成功提示存在 const bodyText = await page.locator('body').textContent(); const hasSuccessIndicator = bodyText!.includes('消息已发送') || bodyText!.includes('感谢') || bodyText!.includes('发送成功') || bodyText!.includes('提交成功'); expect(hasSuccessIndicator).toBe(true); }); }); // ==================== UJ-02: 行业客户浏览解决方案到产品的旅程 ==================== test.describe('UJ-02: 行业客户浏览解决方案到产品的旅程', () => { test('@journey @regression 客户从解决方案浏览到产品详情', async ({ page }) => { // === Step 1: 访问首页 === await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); await closeCookieBanner(page); // 验证首页加载成功 const heroTitle = page.locator('h1').first(); await expect(heroTitle).toBeVisible({ timeout: 10000 }); // === Step 2: 导航到解决方案页面 === // 通过桌面导航的解决方案下拉菜单 const desktopNav = page.locator('[data-testid="desktop-navigation"]').first(); const solutionsDropdown = desktopNav.locator('button:has-text("解决方案")').first(); await expect(solutionsDropdown).toBeVisible({ timeout: 5000 }); await solutionsDropdown.hover(); await page.waitForTimeout(300); const viewAllSolutions = desktopNav.locator('a:has-text("查看全部解决方案")').first(); await expect(viewAllSolutions).toBeVisible({ timeout: 5000 }); await viewAllSolutions.hover(); await page.waitForTimeout(200); await viewAllSolutions.click(); await page.waitForURL(/\/solutions/, { timeout: 10000 }); await expect(page).toHaveURL(/\/solutions/); await page.waitForTimeout(2000); // 验证解决方案页面加载成功 const solutionsTitle = page.locator('h1, h2').first(); await expect(solutionsTitle).toBeVisible(); // === Step 3: 点击一个解决方案 === const solutionLinks = page.locator('a[href*="/solutions/"]:not([href$="/solutions"])'); const solutionLinkCount = await solutionLinks.count(); expect(solutionLinkCount).toBeGreaterThan(0); const firstSolutionLink = solutionLinks.first(); const solutionHref = await firstSolutionLink.getAttribute('href'); console.log('UJ-02 clicking solution link:', solutionHref); await firstSolutionLink.click(); await page.waitForURL(/\/solutions\/.+/, { timeout: 10000 }); await expect(page).toHaveURL(/\/solutions\/.+/); await page.waitForTimeout(2000); // === Step 4: 验证解决方案详情页 === const solutionDetailTitle = page.locator('h1').first(); await expect(solutionDetailTitle).toBeVisible({ timeout: 10000 }); const solutionDetailText = await solutionDetailTitle.textContent(); expect(solutionDetailText).toBeTruthy(); expect(solutionDetailText!.length).toBeGreaterThan(0); // 验证解决方案详情页包含核心内容区域 const solutionMain = page.locator('main').first(); await expect(solutionMain).toBeVisible(); const solutionMainText = await solutionMain.textContent(); expect(solutionMainText!.length).toBeGreaterThan(200); // 验证包含行业痛点/解决方案/价值主张区域 const solutionSections = ['行业痛点', '解决方案', 'Value Proposition']; let hasSolutionSection = false; for (const section of solutionSections) { if (solutionMainText!.includes(section)) { hasSolutionSection = true; break; } } expect(hasSolutionSection).toBe(true); // === Step 5: 通过"查看产品"链接进入产品页面 === // 解决方案详情页的 Hero 区有"查看产品"链接 const viewProductsLink = page.locator('a[href="/products"]').first(); await expect(viewProductsLink).toBeVisible({ timeout: 5000 }); await viewProductsLink.click(); await page.waitForURL(/\/products/, { timeout: 10000 }); await expect(page).toHaveURL(/\/products/); await page.waitForTimeout(2000); // 验证产品页面加载成功 const productsPageTitle = page.locator('h1, h2').first(); await expect(productsPageTitle).toBeVisible(); // === Step 6: 点击产品进入详情页 === const productDetailLinks = page.locator('a[href*="/products/"]:not([href$="/products"])'); const productDetailLinkCount = await productDetailLinks.count(); expect(productDetailLinkCount).toBeGreaterThan(0); const firstProductDetailLink = productDetailLinks.first(); const productHref = await firstProductDetailLink.getAttribute('href'); console.log('UJ-02 clicking product link:', productHref); await firstProductDetailLink.click(); await page.waitForURL(/\/products\/.+/, { timeout: 10000 }); await expect(page).toHaveURL(/\/products\/.+/); await page.waitForTimeout(2000); // 验证产品详情页 const productDetailTitle = page.locator('h1').first(); await expect(productDetailTitle).toBeVisible({ timeout: 10000 }); const productDetailTitleText = await productDetailTitle.textContent(); expect(productDetailTitleText).toBeTruthy(); expect(productDetailTitleText!.length).toBeGreaterThan(0); // 验证产品详情页包含完整内容 const productDetailMain = page.locator('main').first(); const productDetailText = await productDetailMain.textContent(); expect(productDetailText!.length).toBeGreaterThan(200); // 验证存在 CTA 链接 const contactLink = page.locator('a[href="/contact"]').first(); await expect(contactLink).toBeVisible(); }); }); // ==================== UJ-04: 新闻读者浏览旅程 ==================== test.describe('UJ-04: 新闻读者浏览旅程(列表 → 详情 → 返回)', () => { test('@journey @regression 读者从新闻列表浏览到详情页并返回', async ({ page }) => { // === Step 1: 访问新闻列表 === await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); await closeCookieBanner(page); // 验证新闻列表页加载成功 const newsTitle = page.locator('h1').first(); await expect(newsTitle).toBeVisible({ timeout: 10000 }); const newsTitleText = await newsTitle.textContent(); expect(newsTitleText).toBeTruthy(); // 验证新闻列表存在文章条目 const newsListItems = page.locator('a[href*="/news/"]').first(); await expect(newsListItems).toBeVisible({ timeout: 5000 }); // 获取新闻列表中的文章链接数量 const articleLinks = page.locator('a[href*="/news/"]'); const articleCount = await articleLinks.count(); // 可能有些链接是导航/面包屑类,应该有至少1个文章链接 console.log('UJ-04 article links found:', articleCount); if (articleCount > 0) { // === Step 2: 点击第一篇新闻进入详情页 === const firstArticle = articleLinks.first(); await firstArticle.click(); // 等待导航到新闻详情页 await page.waitForURL(/\/news\/.+/, { timeout: 10000 }); await page.waitForTimeout(2000); // === Step 3: 验证新闻详情页 === const detailTitle = page.locator('h1').first(); await expect(detailTitle).toBeVisible({ timeout: 10000 }); const detailTitleText = await detailTitle.textContent(); expect(detailTitleText).toBeTruthy(); expect(detailTitleText!.length).toBeGreaterThan(0); // 验证新闻详情页有发布时间或内容区域 const detailContent = page.locator('main').first(); await expect(detailContent).toBeVisible(); const detailText = await detailContent.textContent(); expect(detailText!.length).toBeGreaterThan(50); // === Step 4: 返回新闻列表 === // 通过面包屑导航返回 const breadcrumbNewsLink = page.locator('nav a[href="/news"], [aria-label*="新闻"] a, a:has-text("新闻")').first(); if (await breadcrumbNewsLink.count() > 0 && await breadcrumbNewsLink.isVisible()) { await breadcrumbNewsLink.click(); await page.waitForURL(/\/news\/?$/, { timeout: 10000 }); await page.waitForTimeout(1000); } else { // 直接导航回新闻列表 await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1000); } // 验证已返回新闻列表 await expect(page).toHaveURL(/\/news\/?$/); } else { // 没有文章链接时,验证页面内容存在 console.log('UJ-04: No article links found, verifying page content'); const bodyText = await page.locator('body').textContent(); expect(bodyText!.length).toBeGreaterThan(0); } }); }); // ==================== UJ-05: 案例浏览者筛选旅程 ==================== test.describe('UJ-05: 案例浏览者筛选旅程(列表 → 筛选 → 详情)', () => { test('@journey @regression 客户从案例列表筛选并查看详情', async ({ page }) => { // === Step 1: 访问案例列表 === await page.goto('/cases', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); await closeCookieBanner(page); // 验证案例列表页加载成功 const casesTitle = page.locator('h1').first(); await expect(casesTitle).toBeVisible({ timeout: 10000 }); const casesTitleText = await casesTitle.textContent(); expect(casesTitleText).toBeTruthy(); console.log('UJ-05 cases page title:', casesTitleText); // === Step 2: 尝试使用行业筛选(如果有筛选功能) === const industryFilters = page.locator( 'button:has-text("全部"), button:has-text("金融"), button:has-text("制造"), ' + 'button:has-text("医疗"), button:has-text("零售"), button:has-text("教育"), ' + 'button:has-text("科技"), [data-testid*="filter"], [data-testid*="industry"]' ); if (await industryFilters.count() > 0) { // 点击第一个可见的筛选按钮(非"全部") const filterButtons = page.locator( 'button:has-text("金融"), button:has-text("制造"), ' + 'button:has-text("医疗"), button:has-text("零售"), ' + 'button:has-text("科技")' ); const filterCount = await filterButtons.count(); if (filterCount > 0) { const firstFilter = filterButtons.first(); const filterText = await firstFilter.textContent(); console.log('UJ-05 clicking filter:', filterText); await firstFilter.click(); await page.waitForTimeout(1500); } } // === Step 3: 点击案例进入详情页 === const caseLinks = page.locator('a[href*="/cases/"]'); const caseLinkCount = await caseLinks.count(); console.log('UJ-05 case links found:', caseLinkCount); if (caseLinkCount > 0) { const firstCase = caseLinks.first(); await firstCase.click(); await page.waitForURL(/\/cases\/.+/, { timeout: 10000 }); await page.waitForTimeout(2000); // === Step 4: 验证案例详情页 === const detailTitle = page.locator('h1').first(); await expect(detailTitle).toBeVisible({ timeout: 10000 }); const detailTitleText = await detailTitle.textContent(); expect(detailTitleText).toBeTruthy(); expect(detailTitleText!.length).toBeGreaterThan(0); // 验证案例详情页包含内容 const detailContent = page.locator('main').first(); await expect(detailContent).toBeVisible(); const detailText = await detailContent.textContent(); expect(detailText!.length).toBeGreaterThan(100); // 验证包含案例相关标识 const hasCaseIndicator = detailText!.includes('案例') || detailText!.includes('成果') || detailText!.includes('挑战') || detailText!.includes('解决方案'); expect(hasCaseIndicator).toBe(true); } else { // 没有案例链接时,验证页面内容存在 console.log('UJ-05: No case links found, verifying page content'); const bodyText = await page.locator('body').textContent(); expect(bodyText!.length).toBeGreaterThan(0); } }); }); // ==================== UJ-10: 深度搜索者旅程 ==================== test.describe('UJ-10: 深度搜索者旅程(分类浏览 → 逐篇阅读 → 内容发现)', () => { test('@journey @regression 用户通过分类筛选浏览多篇文章并发现相关内容', async ({ page }) => { // === Step 1: 访问新闻列表 === await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); await closeCookieBanner(page); // 验证新闻列表页加载成功 const newsTitle = page.locator('h1').first(); await expect(newsTitle).toBeVisible({ timeout: 10000 }); const newsTitleText = await newsTitle.textContent(); expect(newsTitleText).toBeTruthy(); console.log('UJ-10 news title:', newsTitleText); // 验证分类筛选按钮存在 const categoryButtons = page.locator('button:has-text("公司新闻"), button:has-text("研发动态")'); const categoryCount = await categoryButtons.count(); console.log('UJ-10 category buttons found:', categoryCount); expect(categoryCount).toBeGreaterThan(0); // === Step 2: 点击"公司新闻"分类筛选 === const companyNewsBtn = page.locator('button:has-text("公司新闻")').first(); if (await companyNewsBtn.isVisible()) { await companyNewsBtn.click(); await page.waitForTimeout(1000); // 验证筛选后文章列表更新 const filteredArticles = page.locator('a[href*="/news/"]'); const filteredCount = await filteredArticles.count(); console.log('UJ-10 company news articles:', filteredCount); } // === Step 3: 阅读第一篇新闻文章 === const firstArticle = page.locator('a[href*="/news/"]').first(); if (await firstArticle.count() > 0 && await firstArticle.isVisible()) { const firstArticleHref = await firstArticle.getAttribute('href'); console.log('UJ-10 reading first article:', firstArticleHref); await firstArticle.click(); // 等待导航到新闻详情页 await page.waitForURL(/\/news\/.+/, { timeout: 10000 }); await page.waitForTimeout(2000); // 验证新闻详情页加载成功 const detailTitle = page.locator('h1').first(); await expect(detailTitle).toBeVisible({ timeout: 10000 }); const detailTitleText = await detailTitle.textContent(); expect(detailTitleText).toBeTruthy(); expect(detailTitleText!.length).toBeGreaterThan(0); console.log('UJ-10 first article title:', detailTitleText); // 验证详情页有正文内容 const detailContent = page.locator('main').first(); await expect(detailContent).toBeVisible(); const detailText = await detailContent.textContent(); expect(detailText!.length).toBeGreaterThan(100); // 软验证:页面包含发布日期(非关键断言,不阻止测试通过) const dateIndicator = page.locator('time, [datetime], [aria-label*="date"]').first(); const hasDateElement = await dateIndicator.count() > 0; if (hasDateElement) { await expect(dateIndicator).toBeVisible(); } // 验证内容发现:相关新闻或推荐阅读区域 const relatedSection = page.locator( 'text=/相关新闻|推荐阅读|相关文章|RELATED|more news/i' ).first(); if (await relatedSection.count() > 0) { await expect(relatedSection).toBeVisible(); console.log('UJ-10 related news section found'); } } // === Step 4: 返回新闻列表,切换分类浏览 === await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); // 切换到"研发动态"分类 const devNewsBtn = page.locator('button:has-text("研发动态")').first(); if (await devNewsBtn.isVisible()) { await devNewsBtn.click(); await page.waitForTimeout(1000); } // === Step 5: 阅读第二篇新闻文章 === const secondArticle = page.locator('a[href*="/news/"]').first(); if (await secondArticle.count() > 0 && await secondArticle.isVisible()) { const secondArticleHref = await secondArticle.getAttribute('href'); console.log('UJ-10 reading second article:', secondArticleHref); await secondArticle.click(); await page.waitForURL(/\/news\/.+/, { timeout: 10000 }); await page.waitForTimeout(2000); // 验证第二篇新闻详情页 const secondDetailTitle = page.locator('h1').first(); await expect(secondDetailTitle).toBeVisible({ timeout: 10000 }); const secondDetailTitleText = await secondDetailTitle.textContent(); expect(secondDetailTitleText).toBeTruthy(); expect(secondDetailTitleText!.length).toBeGreaterThan(0); console.log('UJ-10 second article title:', secondDetailTitleText); // 验证详情页有正文内容 const secondDetailContent = page.locator('main').first(); await expect(secondDetailContent).toBeVisible(); const secondDetailText = await secondDetailContent.textContent(); expect(secondDetailText!.length).toBeGreaterThan(100); } // === Step 6: 返回全部新闻列表,验证内容发现能力 === await page.goto('/news', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); // 点击"全部"恢复显示所有新闻 const allBtn = page.locator('button:has-text("全部")').first(); if (await allBtn.isVisible()) { await allBtn.click(); await page.waitForTimeout(1000); } // 验证所有新闻文章显示 const allArticles = page.locator('a[href*="/news/"]'); const allCount = await allArticles.count(); console.log('UJ-10 all articles count:', allCount); expect(allCount).toBeGreaterThanOrEqual(1); // 验证搜索功能(内容发现工具) const searchInput = page.locator('input[placeholder*="搜索"]').first(); if (await searchInput.isVisible()) { await searchInput.fill('数字化'); await page.waitForTimeout(1000); const searchResults = page.locator('a[href*="/news/"]'); const searchResultCount = await searchResults.count(); console.log('UJ-10 search results:', searchResultCount); // 搜索功能正常工作,返回结果 expect(searchResultCount).toBeGreaterThanOrEqual(0); } // 最终验证:页面加载正常,内容可发现 const bodyText = await page.locator('body').textContent(); expect(bodyText!.length).toBeGreaterThan(0); console.log('UJ-10 completed successfully'); }); }); // ==================== 辅助函数:Admin 登录 ==================== const ADMIN_CREDENTIALS = { username: 'admin', password: 'admin123' }; const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:3000'; /** * 通过 API 登录 admin 并设置浏览器 cookie 和 localStorage, * 使后续页面导航通过中间件认证,且前端 auth-context 能读取到用户状态。 */ async function loginAdminAndSetCookie(page: Page, request: APIRequestContext): Promise { const loginResponse = await request.post(`${BASE_URL}/api/auth/login`, { data: ADMIN_CREDENTIALS, headers: { 'Content-Type': 'application/json' }, }); expect(loginResponse.ok()).toBeTruthy(); const body = await loginResponse.json(); expect(body.accessToken).toBeTruthy(); const token = body.accessToken as string; // 设置中间件 cookie await page.context().addCookies([ { name: 'novalon_token', value: token, domain: 'localhost', path: '/', httpOnly: true, sameSite: 'Lax', }, ]); // 先导航到 admin 登录页建立同源上下文,再设置 localStorage await page.goto(`${BASE_URL}/admin/login`, { waitUntil: 'domcontentloaded', timeout: 30000 }); // 设置前端 localStorage(auth-context 读取此值判断登录状态) await page.evaluate( ({ token, user }) => { localStorage.setItem('novalon_admin_token', token); localStorage.setItem('novalon_admin_user', JSON.stringify(user)); }, { token, user: body.user } ); return token; } // ==================== UJ-03: 内容管理员完整旅程 ==================== test.describe('UJ-03: 内容管理员完整旅程(登录 → 仪表盘 → 内容管理 → 编辑 → 前台可见)', () => { test('@journey @regression @admin 管理员登录后浏览仪表盘、内容列表、编辑页面,并验证前台可见', async ({ page, request, }) => { test.setTimeout(120000); // === Step 1: 通过 API 登录并设置 cookie === const token = await loginAdminAndSetCookie(page, request); expect(token).toBeTruthy(); console.log('UJ-03 admin login successful'); // === Step 2: 访问 Admin 仪表盘 === await page.goto('/admin', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); // 验证仪表盘加载成功(未重定向到登录页) const currentUrl = page.url(); expect(currentUrl).not.toContain('/admin/login'); console.log('UJ-03 dashboard URL:', currentUrl); // 验证仪表盘标题或内容存在 const bodyText = await page.locator('body').textContent(); expect(bodyText!.length).toBeGreaterThan(0); const hasDashboardContent = bodyText!.includes('仪表盘') || bodyText!.includes('Dashboard') || bodyText!.includes('admin') || bodyText!.includes('管理'); expect(hasDashboardContent).toBe(true); // === Step 3: 导航到新闻内容管理 === const newsContentLink = page.locator('a[href*="/admin/content/news"]').first(); if (await newsContentLink.isVisible()) { await newsContentLink.click(); await page.waitForURL(/\/admin\/content\/news/, { timeout: 10000 }); await page.waitForTimeout(2000); } else { // 直接导航 await page.goto('/admin/content/news', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); } // 验证内容列表页加载成功 const contentUrl = page.url(); expect(contentUrl).toContain('/admin/content/news'); console.log('UJ-03 content list URL:', contentUrl); // 验证页面有内容 const contentBodyText = await page.locator('body').textContent(); expect(contentBodyText!.length).toBeGreaterThan(0); // === Step 4: 通过 API 创建一篇测试新闻 === const suffix = `uj03-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const title = `UJ-03 测试新闻 ${suffix}`; const slug = `uj03-test-news-${suffix}`; // 获取模型 ID const modelsResponse = await request.get(`${BASE_URL}/api/admin/models`, { headers: { Authorization: `Bearer ${token}` }, }); expect(modelsResponse.ok()).toBeTruthy(); const models = await modelsResponse.json(); const newsModel = models.find((m: { code: string }) => m.code === 'news'); expect(newsModel).toBeTruthy(); // 创建新闻 const createResponse = await request.post(`${BASE_URL}/api/admin/items`, { data: { modelId: newsModel.id, modelCode: 'news', title, slug, status: 'draft', data: { id: slug, excerpt: `UJ-03 测试摘要 ${suffix}`, date: new Date().toISOString().split('T')[0], category: '公司新闻', content: `

UJ-03 测试正文内容 ${suffix}

`, featured: false, }, }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); expect(createResponse.ok()).toBeTruthy(); const createdItem = await createResponse.json(); const itemId = createdItem.id; console.log('UJ-03 created item:', itemId, slug); // === Step 5: 导航到编辑页面 === await page.goto(`/admin/content/news/${itemId}`, { waitUntil: 'domcontentloaded', timeout: 30000, }); await page.waitForTimeout(2000); // 验证编辑页面加载成功 const editUrl = page.url(); expect(editUrl).toContain(itemId); const editBodyText = await page.locator('body').textContent(); expect(editBodyText!.length).toBeGreaterThan(0); console.log('UJ-03 editor page loaded'); // === Step 6: 发布内容(API) === // 提交审核 const submitResponse = await request.post( `${BASE_URL}/api/admin/items/${itemId}/workflow`, { data: { action: 'submit' }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, } ); expect(submitResponse.ok()).toBeTruthy(); // 审核通过 const approveResponse = await request.post( `${BASE_URL}/api/admin/items/${itemId}/workflow`, { data: { action: 'approve' }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, } ); expect(approveResponse.ok()).toBeTruthy(); // 刷新 ISR 缓存 const revalidateSecret = process.env.CMS_REVALIDATE_SECRET || 'e2e-test-secret'; await request.post(`${BASE_URL}/api/cms/revalidate`, { data: { event: 'content.published', modelCode: 'news', slug, secret: revalidateSecret, }, headers: { 'Content-Type': 'application/json' }, }); console.log('UJ-03 item published and revalidated'); // === Step 7: 验证前台可见 === await page.goto(`/news/${slug}`, { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); // 验证新闻详情页加载成功且包含标题 await expect .poll( async () => { const h1Text = (await page.locator('h1').first().textContent().catch(() => '')) || ''; return h1Text.includes(title); }, { timeout: 20000, intervals: [1000, 2000, 2000] } ) .toBe(true); // 验证正文内容 await expect(page.locator('body')).toContainText(`UJ-03 测试正文内容 ${suffix}`); console.log('UJ-03 frontend verification passed'); // === Step 8: 清理测试数据 === await request.delete(`${BASE_URL}/api/admin/items?id=${itemId}`, { headers: { Authorization: `Bearer ${token}` }, }); console.log('UJ-03 cleanup completed'); }); }); // ==================== UJ-06: 多角色管理员权限旅程 ==================== test.describe('UJ-06: 多角色管理员权限旅程(超级管理员 → 角色 → 权限 → 用户)', () => { test('@journey @regression @admin 管理员访问角色管理和用户管理页面', async ({ page, request }) => { test.setTimeout(90000); // === Step 1: 登录并设置 cookie === const token = await loginAdminAndSetCookie(page, request); expect(token).toBeTruthy(); console.log('UJ-06 admin login successful'); // === Step 2: 访问角色管理页面 === await page.goto('/admin/roles', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); // 验证未重定向到登录页 const rolesUrl = page.url(); expect(rolesUrl).not.toContain('/admin/login'); console.log('UJ-06 roles URL:', rolesUrl); // 验证角色页面加载成功 const rolesBodyText = await page.locator('body').textContent(); expect(rolesBodyText!.length).toBeGreaterThan(0); // 验证角色列表存在 const hasRoleContent = rolesBodyText!.includes('角色') || rolesBodyText!.includes('super_admin') || rolesBodyText!.includes('权限'); expect(hasRoleContent).toBe(true); // === Step 3: 通过 API 验证角色数据 === const rolesResponse = await request.get(`${BASE_URL}/api/admin/roles`, { headers: { Authorization: `Bearer ${token}` }, }); expect(rolesResponse.ok()).toBeTruthy(); const rolesData = await rolesResponse.json(); const roles = rolesData.roles || rolesData; expect(Array.isArray(roles)).toBe(true); expect(roles.length).toBeGreaterThanOrEqual(5); // super_admin, content_admin, content_editor, reviewer, readonly console.log('UJ-06 roles found:', roles.length); // 验证关键角色存在 const roleCodes = roles.map((r: { code: string }) => r.code); expect(roleCodes).toContain('super_admin'); expect(roleCodes).toContain('content_admin'); expect(roleCodes).toContain('content_editor'); expect(roleCodes).toContain('reviewer'); expect(roleCodes).toContain('readonly'); // === Step 4: 访问用户管理页面 === await page.goto('/admin/users', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); // 验证未重定向到登录页 const usersUrl = page.url(); expect(usersUrl).not.toContain('/admin/login'); console.log('UJ-06 users URL:', usersUrl); // 验证用户页面加载成功 const usersBodyText = await page.locator('body').textContent(); expect(usersBodyText!.length).toBeGreaterThan(0); // 验证用户列表存在 const hasUserContent = usersBodyText!.includes('用户') || usersBodyText!.includes('admin') || usersBodyText!.includes('角色'); expect(hasUserContent).toBe(true); // === Step 5: 通过 API 验证用户数据 === const usersResponse = await request.get(`${BASE_URL}/api/admin/users`, { headers: { Authorization: `Bearer ${token}` }, }); expect(usersResponse.ok()).toBeTruthy(); const usersData = await usersResponse.json(); const users = usersData.users || usersData.data || usersData; expect(Array.isArray(users)).toBe(true); console.log('UJ-06 users found:', users.length); }); }); // ==================== UJ-07: 媒体管理员上传旅程 ==================== test.describe('UJ-07: 媒体管理员上传旅程', () => { test('@journey @regression @admin 管理员访问媒体管理页面并上传文件', async ({ page, request }) => { test.setTimeout(90000); // === Step 1: 登录并设置 cookie === const token = await loginAdminAndSetCookie(page, request); expect(token).toBeTruthy(); console.log('UJ-07 admin login successful'); // === Step 2: 访问媒体管理页面 === await page.goto('/admin/media', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); // 验证未重定向到登录页 const mediaUrl = page.url(); expect(mediaUrl).not.toContain('/admin/login'); console.log('UJ-07 media URL:', mediaUrl); // 验证媒体页面加载成功 const mediaBodyText = await page.locator('body').textContent(); expect(mediaBodyText!.length).toBeGreaterThan(0); // 验证媒体管理相关元素存在 const hasMediaContent = mediaBodyText!.includes('媒体') || mediaBodyText!.includes('上传') || mediaBodyText!.includes('素材') || mediaBodyText!.includes('media'); expect(hasMediaContent).toBe(true); // === Step 3: 通过 API 获取媒体列表 === const mediaResponse = await request.get(`${BASE_URL}/api/admin/media`, { headers: { Authorization: `Bearer ${token}` }, }); expect(mediaResponse.ok()).toBeTruthy(); const mediaData = await mediaResponse.json(); const mediaItems = mediaData.items || []; console.log('UJ-07 existing media items:', mediaItems.length); // === Step 4: 上传测试文件(通过 API POST /api/admin/media 带 FormData) === const testFileName = `uj07-test-${Date.now()}.txt`; const testFileContent = 'UJ-07 test file content'; const uploadResponse = await request.post(`${BASE_URL}/api/admin/media`, { multipart: { file: { name: testFileName, mimeType: 'text/plain', buffer: Buffer.from(testFileContent, 'utf-8'), }, }, headers: { Authorization: `Bearer ${token}`, }, }); expect(uploadResponse.ok()).toBeTruthy(); const uploadedFile = await uploadResponse.json(); // 单文件上传返回资产对象本身 expect(uploadedFile.id).toBeTruthy(); console.log('UJ-07 uploaded file:', uploadedFile.id, uploadedFile.name); // === Step 5: 验证上传文件在媒体列表中可见 === const mediaAfterUploadResponse = await request.get(`${BASE_URL}/api/admin/media`, { headers: { Authorization: `Bearer ${token}` }, }); expect(mediaAfterUploadResponse.ok()).toBeTruthy(); const mediaAfterUpload = await mediaAfterUploadResponse.json(); const mediaAfterItems = mediaAfterUpload.items || []; const foundUploaded = mediaAfterItems.some( (item: { name?: string; url?: string }) => item.name === testFileName || (item.url && item.url.includes(testFileName)) ); expect(foundUploaded).toBe(true); console.log('UJ-07 uploaded file verified in media list'); // === Step 6: 清理上传文件 === if (uploadedFile.id) { await request.delete(`${BASE_URL}/api/admin/media?id=${uploadedFile.id}`, { headers: { Authorization: `Bearer ${token}` }, }); console.log('UJ-07 cleanup completed'); } }); });