diff --git a/.gitignore b/.gitignore index bdb38e5..8ce9def 100644 --- a/.gitignore +++ b/.gitignore @@ -250,6 +250,7 @@ test-email.js test-screenshot.png hero-check.png playwright-test-not-portal.js +heading-hierarchy-report.json # Test reports reports/e2e/ @@ -258,6 +259,7 @@ reports/coverage/ # Performance audit results lighthouse-reports/ +.lighthouseci/ performance-baseline.json # Test data exports diff --git a/README.md b/README.md index a2e2dd2..c8b23cd 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ - [x] 首页 UI 设计优化(2026-07-23):按 `impeccable` 默认流程完成 colorize/harden/layout/typeset/polish,落地 `home-content-v14.tsx`(克制白底 Hero、等宽三列服务卡片、紧凑案例快照、FALLBACK_SERVICES 兜底);更新 chromium desktop/tablet/mobile 视觉基线;修复 `e2e/p5-edge-cases.spec.ts` header 选择器严格模式问题;type-check / lint / unit 942 条 / 视觉 63 条 / 功能 E2E 174 条全部通过 - [x] 顶部导航栏对齐修复(2026-07-23):统一桌面端下拉项与普通链接的 wrapper 结构(`relative -mx-2 px-2 py-2`)与 `inline-flex items-center` 显示方式,修复顶部导航文字不在同一水平线的问题;同步更新 desktop/tablet/mobile/webkit/firefox 视觉基线;chromium/webkit 功能 E2E 与视觉回归全部通过 - [x] CMS / Admin 后台 dogfood 修复与回归(2026-07-25):修复登录后无限重定向、内容保存 400/500、编辑提交 status、原生 confirm 阻塞自动化、表单可访问性、标签输入、操作 Toast 反馈、后台 Cookie 横幅、媒体库空状态等 9 项问题;生成 [`dogfood-cms-regression/report.md`](./dogfood-cms-regression/report.md),19 步回归检查全部通过;type-check / lint(0 errors,172 既有 warnings)/ test:unit(71 suites / 943 tests)全部通过 +- [x] 全站 dogfood 系统性深度测试(2026-07-25):覆盖营销页面与 CMS/Admin 后台,识别 12 项问题(High 3 / Medium 6 / Low 3),生成 [`dogfood-output/report.md`](./dogfood-output/report.md) 及截图/视频证据 +- [x] 全站 dogfood 问题修复(2026-07-25):修复列表详情页导航、CMS 登录状态、服务详情页软 404 等 High 优先级问题;其中软 404 根因定位为 `(marketing)/loading.tsx` 形成的 Loading/Suspense 边界吞掉了动态路由 `notFound()` 抛出的 NEXT_NOT_FOUND 错误,导致未知 slug 返回 HTTP 200,已移除该 loading.tsx 并修复根 `not-found.tsx`;type-check / lint(0 errors,172 既有 warnings)/ test:unit(72 suites / 954 tests)/ 功能 E2E 140 条全部通过 +- [x] dogfood 剩余问题验证与视觉基线更新(2026-07-26):浏览器实测确认主导航「产品/解决方案」下拉菜单 click/hover 均可正常展开,问题已随 `(marketing)/loading.tsx` 移除解决;同步修正 `e2e/nav-dropdown.spec.ts` 中断言文本(`睿新ERP管理系统` → `ERP 管理系统`、`行业方案` → `行业解决方案`);更新 desktop/tablet/mobile/webkit/firefox 全量视觉回归基线(105 张快照);type-check / lint(0 errors,172 既有 warnings)/ test:unit(72 suites / 954 tests)/ 下拉菜单 E2E 全部通过 +- [x] 全量回归与 E2E 配置修复(2026-07-27):修复 `e2e/playwright.config.ts` 中 `storageState` 路径错误(由 `./e2e/storageState.json` 改为基于配置文件目录的绝对路径),该错误导致 firefox/webkit 全量 E2E 运行时无法读取 Cookie 偏好状态而全部失败;将 `e2e/p2-functional-e2e.spec.ts` 中「Footer 导航链接可点击」拆分为隐私政策、服务条款两个独立测试,并改用 `page.evaluate` 直接触发点击,避免 Firefox 中连续全页导航与 Playwright locator 评估的竞态超时;重新执行全量回归:type-check / lint(0 errors,172 既有 warnings)/ test:unit(72 suites / 954 tests)/ E2E 全量(631 passed / 8 skipped / 0 failed,含视觉回归、CMS 工作流)全部通过 ## 技术栈 diff --git a/REGRESSION_REPORT_2026-07-27.md b/REGRESSION_REPORT_2026-07-27.md new file mode 100644 index 0000000..5a2270c --- /dev/null +++ b/REGRESSION_REPORT_2026-07-27.md @@ -0,0 +1,96 @@ +# 全量回归测试报告 — 2026-07-27 + +## 1. 回归目标 + +在完成 dogfood 系统性深度测试及问题修复后,执行全量回归验证,确认: + +1. 所有 dogfood 修复未引入新回归。 +2. 质量门禁(类型检查、Lint、单元测试、E2E、视觉回归)全部通过。 +3. 配置与测试代码中的隐式缺陷被识别并修复。 + +## 2. 测试范围 + +| 类别 | 覆盖内容 | +|------|---------| +| 静态门禁 | TypeScript 类型检查、ESLint | +| 单元测试 | Jest,覆盖 `src/**/*.test.{ts,tsx}` | +| E2E 功能测试 | Playwright:首页、产品、方案、服务、案例、新闻、团队、关于、联系、法律页、导航、表单、404、SEO、A11y、性能、兼容性、边界场景 | +| CMS 工作流测试 | Playwright:管理员发布、非法状态拦截、多角色权限分离 | +| 视觉回归测试 | Playwright:desktop / tablet / mobile / firefox / webkit 全页面与组件截图对比 | + +## 3. 执行环境 + +- **日期**:2026-07-27 +- **分支**:当前工作区(基于 `10404db` 之后的 dogfood 修复集合) +- **Node.js**:v22.23.1 +- **Next.js**:16.2.11 +- **Playwright**:1.58.2 +- **浏览器**:Chromium / Firefox / WebKit + +## 4. 执行结果 + +### 4.1 静态门禁 + +| 检查项 | 命令 | 结果 | +|--------|------|------| +| 类型检查 | `npm run type-check` | ✅ 通过(无错误) | +| 代码风格 | `npm run lint` | ✅ 通过(0 errors,172 既有 warnings) | + +### 4.2 单元测试 + +| 检查项 | 命令 | 结果 | +|--------|------|------| +| 单元测试 | `npm run test:unit` | ✅ 72 suites / 954 tests 全部通过 | + +### 4.3 E2E 全量测试 + +| 检查项 | 命令 | 结果 | +|--------|------|------| +| E2E 全量 | `npm run test` | ✅ 631 passed / 8 skipped / 0 failed | + +8 个 skipped 为 `cms-workflow.spec.ts` 在 Firefox / WebKit 项目中被显式跳过(`test.skip(browserName !== 'chromium')`),属于预期行为,避免跨浏览器共享数据库导致数据竞争。 + +E2E 覆盖明细: + +- 视觉回归:desktop / tablet / mobile / firefox / webkit 全通过 +- CMS 工作流:chromium 下 3 个测试全部通过 +- 跨浏览器兼容性:chromium / firefox / webkit 功能用例全部通过 + +## 5. 回归过程中发现与修复的问题 + +### 5.1 Playwright `storageState` 路径错误 + +- **现象**:首次全量 E2E 运行时,Firefox / WebKit 下所有测试瞬间失败,报错 `Error reading storage state from ./e2e/storageState.json: ENOENT`。 +- **根因**:`e2e/playwright.config.ts` 中 `storageState: './e2e/storageState.json'`,但 `npm run test` 实际执行 `cd e2e && npx playwright test`,导致 Playwright 查找 `e2e/e2e/storageState.json`。 +- **修复**:改为 `path.resolve(__dirname, 'storageState.json')`,以配置文件所在目录为基准,兼容两种执行方式。 +- **验证**:重新执行全量 E2E,Firefox / WebKit 全部通过。 + +### 5.2 Firefox 中 Footer 链接测试 flaky + +- **现象**:`e2e/p2-functional-e2e.spec.ts` 中「Footer 导航链接可点击」在 Firefox 中反复超时,报错 `waiting for "http://localhost:3000/" navigation to finish`。 +- **根因**:Playwright locator API 会在页面存在未完成导航时阻塞;Firefox 对 `window.location.href` 全页导航处理较慢,单次测试内连续两次 `page.goto('/')` + 点击 StaticLink 触发全页导航,容易触发竞态。 +- **修复**: + 1. 将测试拆分为「Footer 导航链接可点击」(隐私政策)和「Footer 服务条款链接可点击」两个独立 test case。 + 2. 改用 `page.evaluate(() => element.click())` 直接触发点击,绕过 locator 的导航状态检查。 + 3. 点击后通过 `page.waitForURL()` 显式等待目标 URL。 +- **验证**:`--project=firefox --repeat-each=3` 6 次运行全部通过;全量 E2E 中该用例通过。 + +## 6. 变更文件清单 + +本次回归修复涉及以下文件变更: + +- `e2e/playwright.config.ts`:修复 `storageState` 路径。 +- `e2e/p2-functional-e2e.spec.ts`:拆分 Footer 链接测试并改用 `page.evaluate` 点击。 +- `README.md`:更新项目进度。 +- `docs/lessons-learned.md`:新增两条测试经验教训。 + +其余未提交修改(如视觉基线快照、dogfood 修复的源码文件)来自先前已完成的修复工作,本次回归验证确认其无新增回归。 + +## 7. 结论 + +全量回归测试通过,所有质量门禁达标,dogfood 修复未引入新回归。项目当前状态满足继续推进或封版条件。 + +--- + +**报告生成时间**:2026-07-27 +**执行者**:AI Agent(基于 `AGENTS.md` 工作流) diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index c02cb91..c892f0f 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -108,6 +108,46 @@ - **方案**:设置覆盖率门槛(branches 35%, functions/lines/statements 45%),分阶段提升至 80%;后续通过 TDD 流程提升至 85%+。 - **来源**:`docs/test-coverage-improvement-plan.md`,`CLAUDE.md` 测试命令。 +### 5.3 E2E 断言文本必须与渲染文本保持一致 + +- **问题**:`e2e/nav-dropdown.spec.ts` 中 hover 测试失败,断言查找 `睿新ERP管理系统` 与 `行业方案`,但实际渲染为 `ERP 管理系统` 与 `行业解决方案`。 +- **根因**:UI 文案迭代后,E2E 测试的文本断言未同步更新,导致测试假阴性(功能正常但测试报错)。 +- **方案**: + 1. 将 UI 文案断言改为稳定标识(如 `data-testid`、角色/aria 属性)或语义化选择器,减少对文案的强依赖。 + 2. 若必须使用文本,在修改文案时同步搜索并更新 `e2e/` 中的对应断言。 + 3. 建立「文案变更检查清单」,将 E2E 文本断言纳入审查范围。 +- **来源**:2026-07-26 dogfood 后续验证,`e2e/nav-dropdown.spec.ts` 修复。 + +### 5.4 视觉回归基线需在 UI 变更后主动更新 + +- **问题**:dogfood 修复后(列表卡片链接、案例筛选、联系表单反馈、新闻占位图等),视觉回归测试大面积失败。 +- **根因**:上述修复属于有意的 UI/交互变更,导致已有快照与当前渲染不一致;若不更新基线,后续所有构建都会报告假阳性。 +- **方案**: + 1. 任何涉及视觉/布局的修复完成后,运行 `npx playwright test visual-regression.spec.ts --update-snapshots` 更新全项目视觉基线。 + 2. 更新前通过浏览器截图/人工复核确认差异符合预期,避免将未发现的回归写入基线。 + 3. 将快照变更作为独立提交或 PR 变更的一部分,方便评审时比对视觉差异。 +- **来源**:2026-07-26 更新 105 张视觉回归快照。 + +### 5.5 Playwright `storageState` 路径必须以配置文件为基准 + +- **问题**:全量 E2E 运行时 firefox/webkit 下所有测试瞬间失败,报错 `Error reading storage state from ./e2e/storageState.json: ENOENT`。 +- **根因**:`e2e/playwright.config.ts` 中 `storageState` 被写成 `./e2e/storageState.json`,而 `npm run test` 实际在 `e2e/` 目录下执行(`cd e2e && npx playwright test`),导致 Playwright 去查找 `e2e/e2e/storageState.json`。 +- **方案**: + 1. 配置文件中的相对路径应基于配置文件自身目录,使用 `path.resolve(__dirname, 'storageState.json')`。 + 2. 修改 `storageState`、快照目录、报告目录等路径时,必须同时验证 `npm run test` 与从项目根目录直接运行 `npx playwright test --config=e2e/playwright.config.ts` 两种方式。 + 3. 将 `storageState.json` 纳入版本控制并作为 E2E 环境准备检查项。 +- **来源**:2026-07-27 全量 E2E 回归失败排查,`e2e/playwright.config.ts` 修复。 + +### 5.6 Firefox 中连续全页导航易触发 Playwright locator 超时 + +- **问题**:`e2e/p2-functional-e2e.spec.ts` 中「Footer 导航链接可点击」在 Firefox 中反复超时,报错 `waiting for "http://localhost:3000/" navigation to finish`,即使页面已渲染完成。 +- **根因**:Playwright 的 locator API 会在页面存在未完成的导航时阻塞;Firefox 对 `window.location.href` 全页导航的处理比 Chromium 更慢,连续两次 `page.goto('/')` + 点击 StaticLink 触发全页导航后,locator 评估极易进入等待导航的竞态。 +- **方案**: + 1. 将涉及两次独立全页导航的测试拆分为两个独立 test case,减少单次测试内的导航次数。 + 2. 在 Firefox 等容易触发竞态的浏览器中,对必须触发的全页导航链接点击,改用 `page.evaluate(() => element.click())` 绕过 locator 的导航状态检查。 + 3. 优先使用 `data-testid` 或语义选择器定位,点击后通过 `page.waitForURL()` 显式等待目标 URL,而非依赖 locator 的隐式等待。 +- **来源**:2026-07-27 `e2e/p2-functional-e2e.spec.ts` Footer 链接测试修复。 + --- ## 6. 代码组织 @@ -128,8 +168,23 @@ --- +## 7. App Router 路由行为 + +### 7.1 `loading.tsx` 会吞掉子动态路由的 `notFound()` + +- **问题**:`/services/[id]`、`/products/[id]`、`/solutions/[id]`、`/news/[slug]` 等动态路由对未知 slug 调用 `notFound()` 后,仍然返回 HTTP 200(软 404)。 +- **根因**:`(marketing)/loading.tsx` 在路由组层级创建了 Next.js Loading/Suspense 边界,该边界会捕获子页面抛出的 `NEXT_NOT_FOUND` 错误,导致 `not-found.tsx` 无法被渲染,HTTP 状态也保持为 200。 +- **方案**:移除 `(marketing)/loading.tsx`;如需加载状态,改为在页面内部使用 `` 包裹特定慢加载区块,而不是在路由组层级全局添加 `loading.tsx`。 +- **验证**:`curl -I http://localhost:3000/services/unknown` 返回 `404 Not Found`;`e2e/p5-edge-cases.spec.ts` 404 相关用例全部通过。 +- **来源**:2026-07-25 dogfood 修复,`dogfood-output/report.md`。 + +--- + ## 更新记录 | 日期 | 更新内容 | 来源 | |------|---------|------| -| 2026-07-07 | 初始创建,从 project_memory.md 和 docs/ 中提取 | 现有文档 + 记忆文件 | \ No newline at end of file +| 2026-07-07 | 初始创建,从 project_memory.md 和 docs/ 中提取 | 现有文档 + 记忆文件 | +| 2026-07-25 | 增加 App Router `loading.tsx` 与 `notFound()` 交互的经验教训 | dogfood 修复 | +| 2026-07-26 | 增加 E2E 文本断言同步、视觉回归基线更新两条经验教训 | dogfood 后续验证 | +| 2026-07-27 | 增加 Playwright storageState 路径、Firefox 全页导航竞态两条经验教训 | 全量 E2E 回归失败排查 | \ No newline at end of file diff --git a/e2e/cases-filter.spec.ts b/e2e/cases-filter.spec.ts new file mode 100644 index 0000000..2620f7f --- /dev/null +++ b/e2e/cases-filter.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test'; + +test('案例页行业筛选按钮可以正确过滤列表', async ({ page }) => { + await page.goto('/cases', { waitUntil: 'domcontentloaded' }); + + // 默认显示全部 6 个案例 + await expect(page.locator('a[href^="/cases/"]')).toHaveCount(6); + + // 点击“制造业”筛选 + await page.getByRole('radio', { name: '制造业' }).click(); + await expect(page.locator('a[href^="/cases/"]')).toHaveCount(1); + await expect(page.locator('text=大型制造企业 ERP 升级与数字化转型')).toBeVisible(); + + // 点击“贸易零售”筛选 + await page.getByRole('radio', { name: '贸易零售' }).click(); + await expect(page.locator('a[href^="/cases/"]')).toHaveCount(1); + await expect(page.locator('text=连锁零售全渠道数字化升级')).toBeVisible(); + + // 切回全部 + await page.getByRole('radio', { name: '全部行业' }).click(); + await expect(page.locator('a[href^="/cases/"]')).toHaveCount(6); +}); diff --git a/e2e/cms-workflow.spec.ts b/e2e/cms-workflow.spec.ts index 6ac1ee6..4044288 100644 --- a/e2e/cms-workflow.spec.ts +++ b/e2e/cms-workflow.spec.ts @@ -1,4 +1,13 @@ import { test, expect, APIRequestContext } from '@playwright/test'; +import { config as dotenvConfig } from 'dotenv'; +import path from 'path'; + +// 加载项目根目录的 .env.local,使 E2E 测试与 dev/preview 服务器使用相同的 CMS_REVALIDATE_SECRET +// Playwright 配置已负责设置 process.env,此处仅做兜底加载,不再 override 以避免覆盖 webServer 环境变量 +dotenvConfig({ path: path.resolve(__dirname, '../.env.local') }); + +// CMS 工作流依赖共享数据库与角色权限,浏览器差异可忽略,仅在 chromium 项目运行以避免跨 project 数据竞争 +test.skip(({ browserName }) => browserName !== 'chromium', 'CMS workflow test runs only in chromium'); /** * CMS 内容发布工作流 E2E 测试 @@ -13,8 +22,60 @@ const REVIEWER_CREDENTIALS = { username: 'e2e_reviewer', password: 'reviewer123' const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:3000'; const REVALIDATE_SECRET = process.env.CMS_REVALIDATE_SECRET || 'e2e-test-secret'; +// 跟踪当前测试创建的内容条目 ID,afterEach 只清理当前测试的数据,避免并行/跨 project 误删 +const createdItemIds: string[] = []; + test.setTimeout(120000); +async function deleteItemsByIds(request: APIRequestContext, token: string, ids: string[]): Promise { + if (ids.length === 0) return; + + await Promise.all( + ids.map((id) => + request.delete(`${BASE_URL}/api/admin/items?id=${id}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + ) + ); +} + +async function cleanupStaleE2ENews(request: APIRequestContext, token: string): Promise { + const response = await request.get(`${BASE_URL}/api/admin/items`, { + headers: { Authorization: `Bearer ${token}` }, + params: { modelCode: 'news', pageSize: '1000' }, + }); + + if (!response.ok()) { + console.warn('Failed to fetch news items for cleanup:', await response.text()); + return; + } + + const body = await response.json(); + const items = (body.items || body.data || []) as Array<{ id: string; title: string }>; + const staleItems = items.filter((item) => item.title?.startsWith('E2E 测试新闻')); + + await deleteItemsByIds( + request, + token, + staleItems.map((item) => item.id) + ); +} + +test.beforeAll(async ({ request }) => { + // 清理历史残留 E2E 测试数据(仅在 chromium 项目执行,不会误删其他 project 数据) + const token = await loginAdmin(request); + await cleanupStaleE2ENews(request, token); +}); + +test.afterEach(async ({ request }) => { + const token = await loginAdmin(request); + // 优先按 ID 清理当前测试创建的条目,避免误删其他并行测试的数据 + const ids = createdItemIds.splice(0); + await deleteItemsByIds(request, token, ids); + // 再兜底清理历史残留 + await cleanupStaleE2ENews(request, token); +}); + async function login( request: APIRequestContext, credentials: { username: string; password: string } @@ -83,6 +144,7 @@ async function createNewsDraft( const body = await response.json(); expect(body.status).toBe('draft'); + createdItemIds.push(body.id); return { id: body.id, slug, title }; } @@ -108,7 +170,7 @@ async function approveItem( request: APIRequestContext, token: string, itemId: string -): Promise { +): Promise<{ status: string; slug: string }> { const response = await request.post(`${BASE_URL}/api/admin/items/${itemId}/workflow`, { data: { action: 'approve' }, headers: { @@ -120,6 +182,7 @@ async function approveItem( expect(response.ok()).toBeTruthy(); const body = await response.json(); expect(body.status).toBe('published'); + return body; } async function revalidateNews(request: APIRequestContext, slug: string): Promise { @@ -133,8 +196,11 @@ async function revalidateNews(request: APIRequestContext, slug: string): Promise headers: { 'Content-Type': 'application/json' }, }); + const responseBody = await response.text(); + console.log('[DEBUG] revalidate status=%s body=%s', response.status(), responseBody); + expect(response.ok()).toBeTruthy(); - const body = await response.json(); + const body = JSON.parse(responseBody); expect(body.code).toBe(0); expect(body.data.revalidatedPaths).toContain('/news'); expect(body.data.revalidatedPaths).toContain(`/news/${slug}`); @@ -162,18 +228,28 @@ async function ensureRolePermissions( async function verifyNewsVisibleOnFrontend( page: import('@playwright/test').Page, + request: APIRequestContext, slug: string, title: string, suffix: string ): Promise { + // 先用 API context 拉取本地页面,确认服务端渲染正常 + const apiResponse = await request.get(`${BASE_URL}/news/${slug}`); + const apiHtml = await apiResponse.text(); + console.log('[DEBUG] api status=%s title=%s', apiResponse.status(), apiHtml.match(/([^<]+)<\/title>/)?.[1]); + // 直接访问详情页验证 ISR 刷新后的内容可见(比列表页更稳定,避免整页缓存波动) await page.goto(`/news/${slug}`, { waitUntil: 'domcontentloaded', timeout: 30000 }); + const currentUrl = page.url(); + const h1Text = (await page.locator('h1').first().textContent().catch(() => '')) || ''; + console.log('[DEBUG] page.url=%s h1=%s expected title=%s', currentUrl, h1Text, title); + await expect .poll( async () => { - const h1Text = (await page.locator('h1').first().textContent().catch(() => '')) || ''; - return h1Text.includes(title); + const text = (await page.locator('h1').first().textContent().catch(() => '')) || ''; + return text.includes(title); }, { timeout: 20000, intervals: [1000, 2000, 2000] } ) @@ -183,15 +259,21 @@ async function verifyNewsVisibleOnFrontend( } test.describe('CMS 内容发布工作流', () => { + // 同一 worker 内串行执行,避免共享 DB/角色权限在并行时相互影响 + test.describe.configure({ mode: 'serial' }); + test('admin 可完成 创建草稿 → 提交审核 → 发布 → 前台可见', async ({ request, page }) => { const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const token = await loginAdmin(request); const { id, slug, title } = await createNewsDraft(request, token, suffix); + console.log('[DEBUG] created item id=%s slug=%s title=%s', id, slug, title); await submitForReview(request, token, id); - await approveItem(request, token, id); + const afterApprove = await approveItem(request, token, id); + console.log('[DEBUG] after approve status=%s slug=%s', afterApprove.status, afterApprove.slug); await revalidateNews(request, slug); - await verifyNewsVisibleOnFrontend(page, slug, title, suffix); + console.log('[DEBUG] revalidate done, navigating to /news/%s', slug); + await verifyNewsVisibleOnFrontend(page, request, slug, title, suffix); }); test('非法状态流转会被拦截', async ({ request }) => { @@ -255,6 +337,6 @@ test.describe('CMS 内容发布工作流', () => { // 5. 刷新 ISR 缓存并验证前台可见 await revalidateNews(request, slug); - await verifyNewsVisibleOnFrontend(page, slug, title, suffix); + await verifyNewsVisibleOnFrontend(page, request, slug, title, suffix); }); }); diff --git a/e2e/nav-dropdown.spec.ts b/e2e/nav-dropdown.spec.ts new file mode 100644 index 0000000..ba2a7bd --- /dev/null +++ b/e2e/nav-dropdown.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; + +test('主导航产品下拉菜单可在 hover 时展开', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); + + const productsButton = page.locator('nav[aria-label="主导航"] button:has-text("产品")'); + await expect(productsButton).toHaveAttribute('aria-expanded', 'false'); + + await productsButton.hover(); + await expect(productsButton).toHaveAttribute('aria-expanded', 'true'); + await expect(page.locator('text=企业套装').first()).toBeVisible(); + await expect(page.locator('text=ERP 管理系统').first()).toBeVisible(); +}); + +test('主导航解决方案下拉菜单可在 hover 时展开', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); + + const solutionsButton = page.locator('nav[aria-label="主导航"] button:has-text("解决方案")'); + await expect(solutionsButton).toHaveAttribute('aria-expanded', 'false'); + + await solutionsButton.hover(); + await expect(solutionsButton).toHaveAttribute('aria-expanded', 'true'); + await expect(page.locator('text=行业解决方案').first()).toBeVisible(); + await expect(page.locator('text=制造业').first()).toBeVisible(); +}); diff --git a/e2e/p2-functional-e2e.spec.ts b/e2e/p2-functional-e2e.spec.ts index 6637eff..444fa95 100644 --- a/e2e/p2-functional-e2e.spec.ts +++ b/e2e/p2-functional-e2e.spec.ts @@ -647,27 +647,46 @@ test.describe('Footer - 全局底部区域', () => { }); test('Footer 导航链接可点击', async ({ page }) => { - const footer = page.locator('footer, [data-testid="footer"]').first(); - - // 检查隐私政策和服务条款链接 - const privacyLink = footer.locator('a:has-text("隐私政策")'); - if (await privacyLink.count() > 0 && await privacyLink.isVisible()) { - await privacyLink.click(); - await page.waitForTimeout(1000); - expect(page.url()).toContain('/privacy'); - } - - // 返回首页检查服务条款 + // 隐私政策链接:独立测试,避免 Firefox 中连续全页导航的竞态问题 await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await page.waitForTimeout(500); + await page.waitForTimeout(1500); - const termsLink = footer.locator('a:has-text("服务条款")'); - if (await termsLink.count() > 0 && await termsLink.isVisible()) { - await termsLink.click(); - await page.waitForTimeout(1000); - expect(page.url()).toContain('/terms'); - } + const privacyClicked = await page.evaluate(() => { + const link = document.querySelector('footer a, [data-testid="footer"] a') as HTMLAnchorElement | null; + if (!link) return false; + const privacy = Array.from(document.querySelectorAll('footer a, [data-testid="footer"] a')) + .find((el) => el.textContent?.includes('隐私政策')) as HTMLAnchorElement | undefined; + if (privacy) { + privacy.scrollIntoView({ behavior: 'instant', block: 'center' }); + privacy.click(); + return true; + } + return false; + }); + + expect(privacyClicked).toBe(true); + await page.waitForURL(/\/privacy/, { timeout: 15000 }); + expect(page.url()).toContain('/privacy'); + }); + + test('Footer 服务条款链接可点击', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); + await page.waitForTimeout(1500); + + const termsClicked = await page.evaluate(() => { + const terms = Array.from(document.querySelectorAll('footer a, [data-testid="footer"] a')) + .find((el) => el.textContent?.includes('服务条款')) as HTMLAnchorElement | undefined; + if (terms) { + terms.scrollIntoView({ behavior: 'instant', block: 'center' }); + terms.click(); + return true; + } + return false; + }); + + expect(termsClicked).toBe(true); + await page.waitForURL(/\/terms/, { timeout: 15000 }); + expect(page.url()).toContain('/terms'); }); test('Footer 包含联系方式(邮箱)', async ({ page }) => { diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 5874351..cf2a5fe 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -1,6 +1,10 @@ import { defineConfig, devices } from '@playwright/test'; +import { config as dotenvConfig } from 'dotenv'; +import path from 'path'; -// 为 CMS 工作流 E2E 测试提供默认 revalidate secret +// 加载项目根目录的 .env.local,使 Playwright webServer 与测试进程使用相同的 CMS_REVALIDATE_SECRET +// 若 .env.local 不存在则回退到默认测试 secret +dotenvConfig({ path: path.resolve(__dirname, '../.env.local') }); process.env.CMS_REVALIDATE_SECRET = process.env.CMS_REVALIDATE_SECRET || 'e2e-test-secret'; export default defineConfig({ @@ -17,7 +21,8 @@ export default defineConfig({ snapshotPathTemplate: '{snapshotDir}/{projectName}/{testFilePath}/{arg}-{projectName}{ext}', use: { baseURL: process.env.E2E_BASE_URL || 'http://localhost:3000', - storageState: './storageState.json', + // 路径以配置文件所在目录为基准,兼容 `cd e2e` 与从项目根目录直接调用两种执行方式 + storageState: path.resolve(__dirname, 'storageState.json'), trace: 'on-first-retry', screenshot: 'only-on-failure', ignoreHTTPSErrors: true, diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/about-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/about-fullpage-visual-chromium-desktop.png index b873cb5..16b5632 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/about-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/about-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-default-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-default-visual-chromium-desktop.png index 94f74bb..7620e9d 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-default-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-default-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-focus-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-focus-visual-chromium-desktop.png index c1af59d..246c284 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-focus-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-focus-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-hover-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-hover-visual-chromium-desktop.png index 1804d09..246c284 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-hover-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/button-hover-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/contact-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/contact-fullpage-visual-chromium-desktop.png index 9a5c2fe..22956a0 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/contact-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/contact-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/header-navigation-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/header-navigation-visual-chromium-desktop.png index 1d7a0e6..fa44cd7 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/header-navigation-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/header-navigation-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/home-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/home-fullpage-visual-chromium-desktop.png index 18147d0..39be905 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/home-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/home-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-desktop.png index 42a3124..c3c7784 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-fullpage-visual-chromium-desktop.png index 776187c..a1c8fe8 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/news-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-desktop.png index ee4dc5d..1a8a757 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/products-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/products-fullpage-visual-chromium-desktop.png index b90d7fe..1f1b0dd 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/products-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/products-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/service-software-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/service-software-fullpage-visual-chromium-desktop.png index 4495eb4..fabd1b6 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/service-software-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/service-software-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/services-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/services-fullpage-visual-chromium-desktop.png index 3e81f07..ccb76a3 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/services-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/services-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-desktop.png index d76aaa6..29398c5 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solutions-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solutions-fullpage-visual-chromium-desktop.png index 1a6a032..d6e7d8a 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solutions-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/solutions-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/team-fullpage-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/team-fullpage-visual-chromium-desktop.png index b655a69..1c46621 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/team-fullpage-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/team-fullpage-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-dark-main-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-dark-main-visual-chromium-desktop.png index 3750cc7..d3d5279 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-dark-main-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-dark-main-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-light-main-visual-chromium-desktop.png b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-light-main-visual-chromium-desktop.png index 3750cc7..d3d5279 100644 Binary files a/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-light-main-visual-chromium-desktop.png and b/e2e/visual-snapshots/visual-chromium-desktop/visual-regression.spec.ts/theme-light-main-visual-chromium-desktop.png differ diff --git a/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-mobile.png b/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-mobile.png index 152da6f..7fa2329 100644 Binary files a/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-mobile.png and b/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-mobile.png differ diff --git a/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-fullpage-visual-chromium-mobile.png b/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-fullpage-visual-chromium-mobile.png index 4749885..3c4fd76 100644 Binary files a/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-fullpage-visual-chromium-mobile.png and b/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/news-fullpage-visual-chromium-mobile.png differ diff --git a/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-mobile.png b/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-mobile.png index f36f4fb..3a9711f 100644 Binary files a/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-mobile.png and b/e2e/visual-snapshots/visual-chromium-mobile/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-mobile.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/about-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/about-fullpage-visual-chromium-tablet.png index 08bc507..f60453c 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/about-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/about-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-default-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-default-visual-chromium-tablet.png index 94b5f5a..cdf134a 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-default-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-default-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-focus-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-focus-visual-chromium-tablet.png index 9de9032..b4e7801 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-focus-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-focus-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-hover-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-hover-visual-chromium-tablet.png index 0e48404..4cf16e0 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-hover-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/button-hover-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/contact-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/contact-fullpage-visual-chromium-tablet.png index 680d822..07db136 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/contact-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/contact-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/header-navigation-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/header-navigation-visual-chromium-tablet.png index 018c160..5958b51 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/header-navigation-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/header-navigation-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/home-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/home-fullpage-visual-chromium-tablet.png index 7c23fff..65e8f9f 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/home-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/home-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-tablet.png index af070f9..7acb848 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-detail-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-fullpage-visual-chromium-tablet.png index d7314f1..c10e877 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/news-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-tablet.png index f29a2e6..59e6702 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/product-erp-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/products-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/products-fullpage-visual-chromium-tablet.png index 893061b..3b2c5be 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/products-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/products-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/service-software-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/service-software-fullpage-visual-chromium-tablet.png index 627f4a9..c3384a5 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/service-software-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/service-software-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/services-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/services-fullpage-visual-chromium-tablet.png index a788606..5bd8f0b 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/services-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/services-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-tablet.png index 97ef5f4..4c04a6e 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solutions-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solutions-fullpage-visual-chromium-tablet.png index feb731e..fcc8d84 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solutions-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/solutions-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/team-fullpage-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/team-fullpage-visual-chromium-tablet.png index 1f53714..6dce5ff 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/team-fullpage-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/team-fullpage-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-dark-main-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-dark-main-visual-chromium-tablet.png index b62ec28..02410cf 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-dark-main-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-dark-main-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-light-main-visual-chromium-tablet.png b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-light-main-visual-chromium-tablet.png index b62ec28..02410cf 100644 Binary files a/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-light-main-visual-chromium-tablet.png and b/e2e/visual-snapshots/visual-chromium-tablet/visual-regression.spec.ts/theme-light-main-visual-chromium-tablet.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/about-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/about-fullpage-visual-firefox-desktop.png index 6552ae0..23cfc1d 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/about-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/about-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-default-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-default-visual-firefox-desktop.png index 944063b..e80f58e 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-default-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-default-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-focus-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-focus-visual-firefox-desktop.png index c0416cf..6e9b824 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-focus-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-focus-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-hover-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-hover-visual-firefox-desktop.png index c0416cf..6e9b824 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-hover-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/button-hover-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/contact-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/contact-fullpage-visual-firefox-desktop.png index 550489a..c532c97 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/contact-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/contact-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/footer-section-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/footer-section-visual-firefox-desktop.png index 2cebe8c..4af6fc5 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/footer-section-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/footer-section-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/header-navigation-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/header-navigation-visual-firefox-desktop.png index cd273a4..fa1e154 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/header-navigation-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/header-navigation-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/home-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/home-fullpage-visual-firefox-desktop.png index 5cd82a7..4446c38 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/home-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/home-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-firefox-desktop.png index 218201c..0b03516 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-fullpage-visual-firefox-desktop.png index fa20444..d060003 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/news-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-firefox-desktop.png index 47f09b3..08b5717 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/products-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/products-fullpage-visual-firefox-desktop.png index b2f886a..64e8990 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/products-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/products-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/service-software-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/service-software-fullpage-visual-firefox-desktop.png index 5b49ff4..393af82 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/service-software-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/service-software-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/services-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/services-fullpage-visual-firefox-desktop.png index 7549eaf..85694e2 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/services-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/services-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-firefox-desktop.png index 2d0bf90..08dadb7 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solutions-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solutions-fullpage-visual-firefox-desktop.png index b8322bc..6f0ea1d 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solutions-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/solutions-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/team-fullpage-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/team-fullpage-visual-firefox-desktop.png index 47d7402..eff3e17 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/team-fullpage-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/team-fullpage-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-dark-main-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-dark-main-visual-firefox-desktop.png index 6367aef..7dae458 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-dark-main-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-dark-main-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-light-main-visual-firefox-desktop.png b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-light-main-visual-firefox-desktop.png index 6367aef..7dae458 100644 Binary files a/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-light-main-visual-firefox-desktop.png and b/e2e/visual-snapshots/visual-firefox-desktop/visual-regression.spec.ts/theme-light-main-visual-firefox-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/about-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/about-fullpage-visual-webkit-desktop.png index a053d1f..71c9f2a 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/about-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/about-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/contact-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/contact-fullpage-visual-webkit-desktop.png index de1b135..c0a7c33 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/contact-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/contact-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/header-navigation-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/header-navigation-visual-webkit-desktop.png index 7d8f6be..1c22fbf 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/header-navigation-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/header-navigation-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/home-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/home-fullpage-visual-webkit-desktop.png index ed5401f..ae2a3c7 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/home-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/home-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-webkit-desktop.png index dd5db49..927abcd 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-detail-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-fullpage-visual-webkit-desktop.png index 912ac5f..02ae751 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/news-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-webkit-desktop.png index 9cabd8b..d64d01d 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/product-erp-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/products-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/products-fullpage-visual-webkit-desktop.png index 28c04e6..aa94c72 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/products-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/products-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/service-software-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/service-software-fullpage-visual-webkit-desktop.png index 90799be..561b53c 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/service-software-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/service-software-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/services-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/services-fullpage-visual-webkit-desktop.png index d7a3063..952ad31 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/services-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/services-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-webkit-desktop.png index aa34fbc..416c495 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solution-manufacturing-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solutions-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solutions-fullpage-visual-webkit-desktop.png index 8afacd8..690a4dd 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solutions-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/solutions-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/team-fullpage-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/team-fullpage-visual-webkit-desktop.png index 018f9f5..1a46628 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/team-fullpage-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/team-fullpage-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-dark-main-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-dark-main-visual-webkit-desktop.png index ed5401f..ae2a3c7 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-dark-main-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-dark-main-visual-webkit-desktop.png differ diff --git a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-light-main-visual-webkit-desktop.png b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-light-main-visual-webkit-desktop.png index ed5401f..ae2a3c7 100644 Binary files a/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-light-main-visual-webkit-desktop.png and b/e2e/visual-snapshots/visual-webkit-desktop/visual-regression.spec.ts/theme-light-main-visual-webkit-desktop.png differ diff --git a/e2e/website-acceptance.spec.ts b/e2e/website-acceptance.spec.ts index aeaa9d3..4e58aa7 100644 --- a/e2e/website-acceptance.spec.ts +++ b/e2e/website-acceptance.spec.ts @@ -14,42 +14,35 @@ test.describe('网站全面测试验收', () => { test('公司Logo可见且不被覆盖', async ({ page }) => { const logo = page.locator('header img[alt*="睿新致远"], [data-testid="logo"]'); const logoCount = await logo.count(); - + + async function assertLogoBoundingBox(logoLocator: ReturnType<typeof page.locator>) { + await expect(logoLocator).toBeVisible({ timeout: 10000 }); + // 等待 layout 完成,避免 WebKit 中 boundingBox() 首次返回 null + const logoBox = await expect.poll(async () => logoLocator.boundingBox(), { + timeout: 5000, + intervals: [100, 200, 200], + }).not.toBeNull(); + + const header = page.locator('header'); + const headerBox = await expect.poll(async () => header.boundingBox(), { + timeout: 5000, + intervals: [100, 200, 200], + }).not.toBeNull(); + + if (logoBox && headerBox) { + expect(logoBox.x).toBeGreaterThanOrEqual(headerBox.x); + expect(logoBox.y).toBeGreaterThanOrEqual(headerBox.y); + expect(logoBox.x + logoBox.width).toBeLessThanOrEqual(headerBox.x + headerBox.width); + expect(logoBox.y + logoBox.height).toBeLessThanOrEqual(headerBox.y + headerBox.height); + } + } + if (logoCount === 0) { const allImages = page.locator('header img'); expect(await allImages.count()).toBeGreaterThan(0); - const firstLogo = allImages.first(); - await expect(firstLogo).toBeVisible(); - - const logoBox = await firstLogo.boundingBox(); - expect(logoBox).not.toBeNull(); - - const header = page.locator('header'); - const headerBox = await header.boundingBox(); - expect(headerBox).not.toBeNull(); - - if (logoBox && headerBox) { - expect(logoBox.x).toBeGreaterThanOrEqual(headerBox.x); - expect(logoBox.y).toBeGreaterThanOrEqual(headerBox.y); - expect(logoBox.x + logoBox.width).toBeLessThanOrEqual(headerBox.x + headerBox.width); - expect(logoBox.y + logoBox.height).toBeLessThanOrEqual(headerBox.y + headerBox.height); - } + await assertLogoBoundingBox(allImages.first()); } else { - await expect(logo.first()).toBeVisible(); - - const logoBox = await logo.first().boundingBox(); - expect(logoBox).not.toBeNull(); - - const header = page.locator('header'); - const headerBox = await header.boundingBox(); - expect(headerBox).not.toBeNull(); - - if (logoBox && headerBox) { - expect(logoBox.x).toBeGreaterThanOrEqual(headerBox.x); - expect(logoBox.y).toBeGreaterThanOrEqual(headerBox.y); - expect(logoBox.x + logoBox.width).toBeLessThanOrEqual(headerBox.x + headerBox.width); - expect(logoBox.y + logoBox.height).toBeLessThanOrEqual(headerBox.y + headerBox.height); - } + await assertLogoBoundingBox(logo.first()); } }); diff --git a/public/images/placeholder-news.jpg b/public/images/placeholder-news.jpg new file mode 100644 index 0000000..e7b4c3d Binary files /dev/null and b/public/images/placeholder-news.jpg differ diff --git a/src/app/(marketing)/contact/contact-content-v3.tsx b/src/app/(marketing)/contact/contact-content-v3.tsx index d49f3d2..f5c835f 100644 --- a/src/app/(marketing)/contact/contact-content-v3.tsx +++ b/src/app/(marketing)/contact/contact-content-v3.tsx @@ -172,7 +172,7 @@ function ContactFormContent({ data }: { data: ContactData }) { company: formData.subject, }); trackConversion('contact_form_submission'); - setToastMessage(data.successMessage || ''); + setToastMessage(data.successMessage || '提交成功!我们会尽快与您联系。'); setToastType('success'); setShowToast(true); setIsSubmitted(true); @@ -373,8 +373,8 @@ function ContactFormContent({ data }: { data: ContactData }) { <div className="w-20 h-20 bg-brand rounded-full flex items-center justify-center mx-auto mb-6"> <CheckCircle2 className="w-10 h-10 text-white" /> </div> - <h4 className="text-2xl font-bold text-ink mb-3">{data.successTitle || ''}</h4> - <p className="text-text-secondary">{data.successMessage || ''}</p> + <h4 className="text-2xl font-bold text-ink mb-3">{data.successTitle || '提交成功'}</h4> + <p className="text-text-secondary">{data.successMessage || '感谢您的留言,我们会尽快与您联系。'}</p> </div> ) : ( <form onSubmit={handleSubmit} className="space-y-6" noValidate> diff --git a/src/app/(marketing)/loading.tsx b/src/app/(marketing)/loading.tsx deleted file mode 100644 index fb9699c..0000000 --- a/src/app/(marketing)/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function Loading() { - return ( - <div className="min-h-[60vh] flex items-center justify-center"> - <div className="page-transition-loader"> - <div className="page-transition-loader__bar" /> - </div> - </div> - ); -} diff --git a/src/app/(marketing)/news/[slug]/page.tsx b/src/app/(marketing)/news/[slug]/page.tsx index 77d8f33..0fe3b4e 100644 --- a/src/app/(marketing)/news/[slug]/page.tsx +++ b/src/app/(marketing)/news/[slug]/page.tsx @@ -5,8 +5,8 @@ import { getPublishedItemBySlug, getPublishedItems, getAllPublishedSlugs } from import type { NewsItem } from '@/lib/constants/news'; import NewsDetailContentV3 from '../news-detail-content-v3'; -// ISR:生产环境每 1 小时自动重新验证,CMS 发布/更新时通过 /api/cms/revalidate 主动刷新 -export const revalidate = 3600; +// 使用 force-dynamic 确保未找到 slug 返回硬 404,同时允许 CMS 新发布内容在首次请求时动态渲染 +export const dynamic = 'force-dynamic'; export async function generateStaticParams() { const slugs = await getAllPublishedSlugs('news'); diff --git a/src/app/(marketing)/news/news-content-v3.tsx b/src/app/(marketing)/news/news-content-v3.tsx index c4a48f3..5ceca6d 100644 --- a/src/app/(marketing)/news/news-content-v3.tsx +++ b/src/app/(marketing)/news/news-content-v3.tsx @@ -4,6 +4,7 @@ import { useState, useMemo, ChangeEvent } from 'react'; import { motion } from 'framer-motion'; import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal'; import { Button } from '@/components/ui/button'; +import { StaticLink } from '@/components/ui/static-link'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Search, Calendar, ArrowRight, Newspaper, ChevronLeft, ChevronRight } from 'lucide-react'; @@ -15,6 +16,7 @@ const categories = ['全部', '公司新闻', '研发动态']; const ITEMS_PER_PAGE = 9; function NewsCard({ newsItem, index }: { newsItem: NewsItem; index: number }) { + const [imageError, setImageError] = useState(false); return ( <motion.div initial={{ opacity: 0, y: 20 }} @@ -22,17 +24,18 @@ function NewsCard({ newsItem, index }: { newsItem: NewsItem; index: number }) { viewport={{ once: true, margin: '-80px' }} transition={{ duration: 0.5, delay: index * 0.08, ease: EASE_OUT }} > - <a + <StaticLink href={`/news/${newsItem.id}`} className="group relative block border border-border-primary hover:border-brand/30 bg-white transition-all duration-500 hover:-translate-y-2 overflow-hidden h-full" > <div className="absolute top-0 left-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" /> - {newsItem.image ? ( + {newsItem.image && !imageError ? ( <div className="aspect-video bg-bg-secondary overflow-hidden"> <img src={newsItem.image} alt={newsItem.title} + onError={() => setImageError(true)} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" /> </div> @@ -66,7 +69,7 @@ function NewsCard({ newsItem, index }: { newsItem: NewsItem; index: number }) { <ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" /> </div> </div> - </a> + </StaticLink> </motion.div> ); } diff --git a/src/app/(marketing)/products/[id]/page.tsx b/src/app/(marketing)/products/[id]/page.tsx index e742a1c..25e1854 100644 --- a/src/app/(marketing)/products/[id]/page.tsx +++ b/src/app/(marketing)/products/[id]/page.tsx @@ -5,7 +5,8 @@ import { getPublishedItemBySlug, getAllPublishedSlugs } from '@/lib/cms/data-ser import type { Product } from '@/lib/constants/products'; import ProductDetailContentV3 from '../product-detail-content-v3'; -export const revalidate = 3600; +// 使用 force-dynamic 确保未找到 slug 返回硬 404,同时允许 CMS 新发布内容在首次请求时动态渲染 +export const dynamic = 'force-dynamic'; export async function generateStaticParams() { const slugs = await getAllPublishedSlugs('product'); diff --git a/src/app/(marketing)/products/products-content-v3.tsx b/src/app/(marketing)/products/products-content-v3.tsx index f8965fe..da745ce 100644 --- a/src/app/(marketing)/products/products-content-v3.tsx +++ b/src/app/(marketing)/products/products-content-v3.tsx @@ -5,6 +5,7 @@ import { motion, useInView } from 'framer-motion'; import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal'; import { EASE_OUT } from '@/components/ui/page-decoration'; import { Button } from '@/components/ui/button'; +import { StaticLink } from '@/components/ui/static-link'; import { ArrowRight, Package, Cpu, BarChart3, Users, Settings, FileText } from 'lucide-react'; import type { Product } from '@/lib/constants/products'; import { useReducedMotion } from '@/hooks/use-reduced-motion'; @@ -149,45 +150,48 @@ function ProductCard({ product }: { product: Product }) { const bundleLabel = product.bundle === 'enterprise' ? '企业套装' : '专业产品'; return ( - <motion.a - href={`/products/${product.id}`} + <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: '-60px' }} transition={{ duration: 0.6, ease: EASE_OUT }} - className="group block overflow-hidden border border-border-primary hover:border-border-secondary bg-white hover:bg-bg-secondary transition-all duration-500 flex flex-col h-full" > - <div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1"> - <div className="flex items-start justify-between mb-4"> - <div> - <div className="text-xs text-text-muted mb-1">{bundleLabel}</div> - <h3 className="text-lg font-bold text-text-primary">{product.title}</h3> - </div> - <span className="px-2 py-1 text-xs bg-brand/10 text-brand" aria-hidden="true">{bundleLabel}</span> - </div> - - <p className="text-sm text-text-secondary mb-4 leading-relaxed flex-1"> - {product.scenario || product.description} - </p> - - <div className="grid grid-cols-3 gap-2 mb-4"> - {product.metrics?.map((m) => ( - <div key={m.label} className="text-center p-2 bg-bg-secondary"> - <div className="text-lg font-bold text-text-primary">{m.value}</div> - <div className="text-xs text-text-muted">{m.label}</div> + <StaticLink + href={`/products/${product.id}`} + className="group block overflow-hidden border border-border-primary hover:border-border-secondary bg-white hover:bg-bg-secondary transition-all duration-500 flex flex-col h-full" + > + <div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1"> + <div className="flex items-start justify-between mb-4"> + <div> + <div className="text-xs text-text-muted mb-1">{bundleLabel}</div> + <h3 className="text-lg font-bold text-text-primary">{product.title}</h3> </div> - ))} - </div> + <span className="px-2 py-1 text-xs bg-brand/10 text-brand" aria-hidden="true">{bundleLabel}</span> + </div> - <div className="flex flex-wrap gap-2"> - {product.capabilities?.map((cap) => ( - <span key={cap} className="px-2 py-1 text-xs border border-border-primary text-text-secondary"> - {cap} - </span> - ))} + <p className="text-sm text-text-secondary mb-4 leading-relaxed flex-1"> + {product.scenario || product.description} + </p> + + <div className="grid grid-cols-3 gap-2 mb-4"> + {product.metrics?.map((m) => ( + <div key={m.label} className="text-center p-2 bg-bg-secondary"> + <div className="text-lg font-bold text-text-primary">{m.value}</div> + <div className="text-xs text-text-muted">{m.label}</div> + </div> + ))} + </div> + + <div className="flex flex-wrap gap-2"> + {product.capabilities?.map((cap) => ( + <span key={cap} className="px-2 py-1 text-xs border border-border-primary text-text-secondary"> + {cap} + </span> + ))} + </div> </div> - </div> - </motion.a> + </StaticLink> + </motion.div> ); } diff --git a/src/app/(marketing)/services/[id]/page.tsx b/src/app/(marketing)/services/[id]/page.tsx index e675655..8db03cd 100644 --- a/src/app/(marketing)/services/[id]/page.tsx +++ b/src/app/(marketing)/services/[id]/page.tsx @@ -5,8 +5,8 @@ import { getPublishedItemBySlug, getAllPublishedSlugs } from '@/lib/cms/data-ser import type { Service } from '@/lib/constants/services'; import { ServiceDetailClient } from './client'; -// ISR:生产环境每 1 小时自动重新验证,CMS 发布/更新时通过 /api/cms/revalidate 主动刷新 -export const revalidate = 3600; +// 使用 force-dynamic 确保未找到 slug 返回硬 404,同时允许 CMS 新发布内容在首次请求时动态渲染 +export const dynamic = 'force-dynamic'; export async function generateStaticParams() { const slugs = await getAllPublishedSlugs('service'); @@ -38,4 +38,4 @@ export default async function ServiceDetailPage({ params }: { params: Promise<{ const service = { ...item.data, title: item.data.title || item.title } as unknown as Service; return <ServiceDetailClient service={JSON.parse(JSON.stringify(service))} />; -} \ No newline at end of file +} diff --git a/src/app/(marketing)/services/services-content-v3.tsx b/src/app/(marketing)/services/services-content-v3.tsx index ed2c87a..85664ed 100644 --- a/src/app/(marketing)/services/services-content-v3.tsx +++ b/src/app/(marketing)/services/services-content-v3.tsx @@ -2,6 +2,7 @@ import { motion } from 'framer-motion'; import { ArrowUpRight, CheckCircle2 } from 'lucide-react'; +import { StaticLink } from '@/components/ui/static-link'; import type { Service } from '@/lib/constants/services'; const EASE_OUT = [0.22, 1, 0.36, 1] as const; @@ -192,48 +193,51 @@ function ServicesGridSection({ services }: { services?: Service[] }) { <div className="grid md:grid-cols-2 gap-px bg-border-primary"> {displayServices.map((service, i) => ( - <motion.a + <motion.div key={service.id} - href={`/services/${service.id}`} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: '-60px' }} transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }} - className="group relative block p-8 sm:p-10 md:p-12 transition-all duration-500 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-white border border-transparent hover:border-border-primary" > - <div className="relative z-10"> - <h3 className="text-xl sm:text-2xl font-bold text-ink group-hover:text-brand transition-colors duration-300 mb-3"> - {service.title} - </h3> + <StaticLink + href={`/services/${service.id}`} + className="group relative block p-8 sm:p-10 md:p-12 transition-all duration-500 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-white border border-transparent hover:border-border-primary" + > + <div className="relative z-10"> + <h3 className="text-xl sm:text-2xl font-bold text-ink group-hover:text-brand transition-colors duration-300 mb-3"> + {service.title} + </h3> - <p className="text-text-secondary leading-relaxed mb-6 text-[15px]"> - {service.description} - </p> + <p className="text-text-secondary leading-relaxed mb-6 text-[15px]"> + {service.description} + </p> - <ul className="space-y-2 mb-6"> - {service.capabilities.map((cap) => ( - <li key={cap} className="flex items-start gap-2 text-sm text-text-secondary"> - <CheckCircle2 className="w-4 h-4 text-brand shrink-0 mt-0.5" /> - {cap} - </li> - ))} - </ul> + <ul className="space-y-2 mb-6"> + {service.capabilities.map((cap) => ( + <li key={cap} className="flex items-start gap-2 text-sm text-text-secondary"> + <CheckCircle2 className="w-4 h-4 text-brand shrink-0 mt-0.5" /> + {cap} + </li> + ))} + </ul> - <div className="flex gap-6 pt-5 border-t border-border-primary mb-6"> - {service.metrics.map((m) => ( - <div key={m.label}> - <div className="text-xl font-bold text-ink">{m.value}</div> - <div className="text-xs text-text-muted mt-1">{m.label}</div> - </div> - ))} + <div className="flex gap-6 pt-5 border-t border-border-primary mb-6"> + {service.metrics.map((m) => ( + <div key={m.label}> + <div className="text-xl font-bold text-ink">{m.value}</div> + <div className="text-xs text-text-muted mt-1">{m.label}</div> + </div> + ))} + </div> + + <div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300"> + <span>了解详情</span> + <ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" /> + </div> </div> - - <div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300"> - <span>了解详情</span> - <ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" /> - </div> - </div> - </motion.a> + </StaticLink> + </motion.div> ))} </div> </div> diff --git a/src/app/(marketing)/solutions/[id]/page.tsx b/src/app/(marketing)/solutions/[id]/page.tsx index 5e01a8c..f938920 100644 --- a/src/app/(marketing)/solutions/[id]/page.tsx +++ b/src/app/(marketing)/solutions/[id]/page.tsx @@ -6,7 +6,8 @@ import type { Solution } from '@/lib/constants/solutions'; import type { Product } from '@/lib/constants/products'; import { SolutionDetailClient } from './client'; -export const revalidate = 3600; +// 使用 force-dynamic 确保未找到 slug 返回硬 404,同时允许 CMS 新发布内容在首次请求时动态渲染 +export const dynamic = 'force-dynamic'; export async function generateStaticParams() { const slugs = await getAllPublishedSlugs('solution'); diff --git a/src/app/(marketing)/solutions/solutions-content-v3.tsx b/src/app/(marketing)/solutions/solutions-content-v3.tsx index 09a2051..df53c4d 100644 --- a/src/app/(marketing)/solutions/solutions-content-v3.tsx +++ b/src/app/(marketing)/solutions/solutions-content-v3.tsx @@ -2,6 +2,7 @@ import { motion } from 'framer-motion'; import { ArrowUpRight, Factory, ShoppingCart, Heart, GraduationCap, Shield, Truck, type LucideIcon } from 'lucide-react'; +import { StaticLink } from '@/components/ui/static-link'; import { type Solution } from '@/lib/constants/solutions'; import type { Product } from '@/lib/constants/products'; @@ -115,55 +116,58 @@ function SolutionCard({ solution, index, products }: { solution: Solution; index .filter(Boolean) as string[]; return ( - <motion.a - href={`/solutions/${solution.id}`} + <motion.div initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: '-60px' }} transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }} - className="group relative block transition-all duration-500 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-white border border-border-primary hover:border-brand/20" > - <div className="relative z-10 p-8 sm:p-10 md:p-12"> - <div className="flex items-center gap-3 mb-6"> - <Icon className="w-5 h-5 text-text-muted" strokeWidth={1.5} /> - <h3 className="text-lg font-bold text-text-primary"> - {solution.industry} - </h3> - </div> + <StaticLink + href={`/solutions/${solution.id}`} + className="group relative block transition-all duration-500 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-white border border-border-primary hover:border-brand/20" + > + <div className="relative z-10 p-8 sm:p-10 md:p-12"> + <div className="flex items-center gap-3 mb-6"> + <Icon className="w-5 h-5 text-text-muted" strokeWidth={1.5} /> + <h3 className="text-lg font-bold text-text-primary"> + {solution.industry} + </h3> + </div> - <div className="mb-4"> - <div className="text-xs text-text-muted mb-2">核心痛点</div> - <p className="text-sm text-text-secondary leading-relaxed"> - {solution.painPoints?.join('、')} - </p> - </div> + <div className="mb-4"> + <div className="text-xs text-text-muted mb-2">核心痛点</div> + <p className="text-sm text-text-secondary leading-relaxed"> + {solution.painPoints?.join('、')} + </p> + </div> - <div className="grid grid-cols-2 gap-2 mb-4"> - {solution.outcomes?.map((o) => ( - <div key={o.label} className="p-2 bg-bg-secondary"> - <div className="text-base font-bold text-brand">{o.value}</div> - <div className="text-xs text-text-muted">{o.label}</div> - </div> - ))} - </div> - - <div className="pt-4 border-t border-border-primary"> - <div className="text-xs text-text-muted mb-2">推荐产品组合</div> - <div className="flex flex-wrap gap-2"> - {recommendedProductNames.map((name) => ( - <span key={name} className="px-2 py-1 text-xs bg-bg-secondary text-text-secondary"> - {name.replace('睿新', '')} - </span> + <div className="grid grid-cols-2 gap-2 mb-4"> + {solution.outcomes?.map((o) => ( + <div key={o.label} className="p-2 bg-bg-secondary"> + <div className="text-base font-bold text-brand">{o.value}</div> + <div className="text-xs text-text-muted">{o.label}</div> + </div> ))} </div> - </div> - <div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300 mt-6"> - <span>了解详情</span> - <ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" /> + <div className="pt-4 border-t border-border-primary"> + <div className="text-xs text-text-muted mb-2">推荐产品组合</div> + <div className="flex flex-wrap gap-2"> + {recommendedProductNames.map((name) => ( + <span key={name} className="px-2 py-1 text-xs bg-bg-secondary text-text-secondary"> + {name.replace('睿新', '')} + </span> + ))} + </div> + </div> + + <div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300 mt-6"> + <span>了解详情</span> + <ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" /> + </div> </div> - </div> - </motion.a> + </StaticLink> + </motion.div> ); } diff --git a/src/lib/auth.test.ts b/src/lib/auth.test.ts new file mode 100644 index 0000000..f3d0f6b --- /dev/null +++ b/src/lib/auth.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, jest, beforeEach } from '@jest/globals'; + +process.env.JWT_SECRET = 'test-jwt-secret'; +process.env.JWT_REFRESH_SECRET = 'test-refresh-secret'; + +jest.mock('jsonwebtoken', () => ({ + verify: jest.fn(), + sign: jest.fn(), +})); + +jest.unmock('./auth'); + +import jwt from 'jsonwebtoken'; +import { NextRequest } from 'next/server'; +import { authenticateRequest, getTokenFromRequest, getTokenFromCookie } from './auth'; +import type { JwtPayload } from './auth'; + +const mockJwtVerify = jwt.verify as jest.MockedFunction<typeof jwt.verify>; + +type NextRequestInit = NonNullable<ConstructorParameters<typeof NextRequest>[1]>; + +function createRequest(init?: NextRequestInit): NextRequest { + const request = new NextRequest('http://localhost/api/admin/models', init); + const cookieHeader = init?.headers && 'cookie' in init.headers ? init.headers.cookie : undefined; + const cookieMap = new Map<string, string>(); + if (typeof cookieHeader === 'string') { + cookieHeader.split(';').forEach((pair) => { + const [name, value] = pair.trim().split('='); + if (name && value !== undefined) cookieMap.set(name, value); + }); + } + Object.defineProperty(request, 'cookies', { + value: { + get: (name: string) => { + const value = cookieMap.get(name); + return value !== undefined ? { name, value } : undefined; + }, + }, + configurable: true, + }); + return request; +} + +describe('getTokenFromRequest', () => { + it('returns token from Authorization header', () => { + const request = createRequest({ + headers: { Authorization: 'Bearer valid-token' }, + }); + expect(getTokenFromRequest(request)).toBe('valid-token'); + }); + + it('returns null when Authorization header is missing', () => { + const request = createRequest(); + expect(getTokenFromRequest(request)).toBeNull(); + }); + + it('returns null when Authorization header is not Bearer', () => { + const request = createRequest({ + headers: { Authorization: 'Basic valid-token' }, + }); + expect(getTokenFromRequest(request)).toBeNull(); + }); +}); + +describe('getTokenFromCookie', () => { + it('returns token from novalon_token cookie', () => { + const request = createRequest({ + headers: { cookie: 'novalon_token=cookie-token; other=value' }, + }); + expect(getTokenFromCookie(request)).toBe('cookie-token'); + }); + + it('returns null when novalon_token cookie is missing', () => { + const request = createRequest({ + headers: { cookie: 'other=value' }, + }); + expect(getTokenFromCookie(request)).toBeNull(); + }); + + it('returns null when no cookies are present', () => { + const request = createRequest(); + expect(getTokenFromCookie(request)).toBeNull(); + }); +}); + +describe('authenticateRequest', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('authenticates from Authorization header', () => { + const payload: JwtPayload = { userId: 'user-1', username: 'admin', role: 'super_admin' }; + mockJwtVerify.mockReturnValue(payload as unknown as void); + + const request = createRequest({ + headers: { Authorization: 'Bearer header-token' }, + }); + + expect(authenticateRequest(request)).toEqual(payload); + expect(mockJwtVerify).toHaveBeenCalledWith('header-token', 'test-jwt-secret'); + }); + + it('falls back to cookie when Authorization header is missing', () => { + const payload: JwtPayload = { userId: 'user-2', username: 'editor', role: 'editor' }; + mockJwtVerify.mockReturnValue(payload as unknown as void); + + const request = createRequest({ + headers: { cookie: 'novalon_token=cookie-token' }, + }); + + expect(authenticateRequest(request)).toEqual(payload); + expect(mockJwtVerify).toHaveBeenCalledWith('cookie-token', 'test-jwt-secret'); + }); + + it('prefers Authorization header over cookie when both are present', () => { + const payload: JwtPayload = { userId: 'user-3', username: 'admin', role: 'super_admin' }; + mockJwtVerify.mockReturnValue(payload as unknown as void); + + const request = createRequest({ + headers: { + Authorization: 'Bearer header-token', + cookie: 'novalon_token=cookie-token', + }, + }); + + authenticateRequest(request); + expect(mockJwtVerify).toHaveBeenCalledWith('header-token', 'test-jwt-secret'); + }); + + it('returns null when no token is present', () => { + const request = createRequest(); + expect(authenticateRequest(request)).toBeNull(); + expect(mockJwtVerify).not.toHaveBeenCalled(); + }); + + it('returns null when token verification fails', () => { + mockJwtVerify.mockImplementation(() => { + throw new Error('invalid token'); + }); + + const request = createRequest({ + headers: { Authorization: 'Bearer invalid-token' }, + }); + + expect(authenticateRequest(request)).toBeNull(); + }); +}); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index c4273f4..451f707 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -47,6 +47,8 @@ export function verifyRefreshToken(token: string): JwtPayload { return jwt.verify(token, JWT_REFRESH_SECRET!) as JwtPayload; } +const ACCESS_TOKEN_COOKIE = 'novalon_token'; + // ============ 请求认证 ============ export function getTokenFromRequest(request: NextRequest): string | null { @@ -57,8 +59,19 @@ export function getTokenFromRequest(request: NextRequest): string | null { return null; } +/** + * 从请求 cookie 中读取 access token。 + * 中间件(Edge Runtime)与 API 路由(Node Runtime)均使用同一 cookie 名, + * 保证 SSR/页面路由与 API 调用的认证状态一致。 + */ +export function getTokenFromCookie(request: NextRequest): string | null { + return request.cookies.get(ACCESS_TOKEN_COOKIE)?.value ?? null; +} + export function authenticateRequest(request: NextRequest): JwtPayload | null { - const token = getTokenFromRequest(request); + // 优先使用 Authorization 头(adminApi 客户端显式携带),同时兼容 cookie 认证 + // 使中间件与 API 路由共享同一认证来源,避免 SSR/客户端状态不一致。 + const token = getTokenFromRequest(request) || getTokenFromCookie(request); if (!token) return null; try { return verifyAccessToken(token); @@ -85,11 +98,11 @@ export function setTokenCookie( refreshToken: string, secure = false, ): void { - response.headers.set('Set-Cookie', buildCookie('novalon_token', accessToken, 86400, secure)); + response.headers.set('Set-Cookie', buildCookie(ACCESS_TOKEN_COOKIE, accessToken, 86400, secure)); response.headers.append('Set-Cookie', buildCookie('novalon_refresh', refreshToken, 604800, secure)); } export function clearTokenCookie(response: Response, secure = false): void { - response.headers.set('Set-Cookie', buildCookie('novalon_token', '', 0, secure)); + response.headers.set('Set-Cookie', buildCookie(ACCESS_TOKEN_COOKIE, '', 0, secure)); response.headers.append('Set-Cookie', buildCookie('novalon_refresh', '', 0, secure)); } \ No newline at end of file diff --git a/src/lib/constants/navigation.ts b/src/lib/constants/navigation.ts index 0b57199..b0cc9cd 100644 --- a/src/lib/constants/navigation.ts +++ b/src/lib/constants/navigation.ts @@ -47,6 +47,7 @@ export const NAVIGATION_V2: NavigationItemV2[] = [ { id: 'solutions', label: '解决方案', href: '/solutions', hasDropdown: true, dropdownKey: 'solutions' }, { id: 'services', label: '服务', href: '/services' }, { id: 'cases', label: '案例', href: '/cases' }, + { id: 'news', label: '新闻动态', href: '/news' }, { id: 'about', label: '关于我们', href: '/about' }, { id: 'contact', label: '联系我们', href: '/contact' }, ];