From 6041d717f7dab37081e605855b5b643299f3a593 Mon Sep 17 00:00:00 2001 From: cneicy Date: Fri, 6 Nov 2026 19:40:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E7=94=A8=E6=88=B7=20ID=20=E5=A4=8D=E5=88=B6=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=8D=A2=E8=A1=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 禁止用户 ID 在按钮内拆行,确保长用户名场景下按钮整体换行 - 添加 Chromium 和 Firefox 在不同视口宽度下的布局回归测试 - 更新 CI 浏览器安装配置及测试文档 - 忽略包含生产服务器结构信息的本地运维技能目录 --- .gitea/workflows/frontend.yml | 2 +- .gitignore | 3 + app/assets/styles/pages.DytekQXu.css | 3 + tests/README.md | 2 +- tests/review-style-layout.e2e.test.mjs | 93 ++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/frontend.yml b/.gitea/workflows/frontend.yml index b38d4eb..b1485a4 100644 --- a/.gitea/workflows/frontend.yml +++ b/.gitea/workflows/frontend.yml @@ -21,7 +21,7 @@ jobs: - name: 安装锁定依赖 run: npm ci - name: 安装测试浏览器 - run: npx playwright install --with-deps chromium + run: npx playwright install --with-deps chromium firefox - name: 检查源码并构建 run: | npm run check diff --git a/.gitignore b/.gitignore index 37fd9b2..7bdbe17 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ test-results/ # 本地交接、审计报告和验收截图不随源码交付;README 保留项目使用说明。 docs/ + +# 本地运维 skill 含生产服务器结构说明,只留在操作者本机。 +.claude/skills/crashwork-ops/ diff --git a/app/assets/styles/pages.DytekQXu.css b/app/assets/styles/pages.DytekQXu.css index 60367fc..02800aa 100644 --- a/app/assets/styles/pages.DytekQXu.css +++ b/app/assets/styles/pages.DytekQXu.css @@ -123,7 +123,10 @@ .welcome-body[data-v-b43b228e] > .p-message { margin: 0 26px 22px; } +/* 用户 ID 是不可拆分的整体:不继承标题的任意断行,长用户名时整个按钮换到下一行。 */ .copy-id[data-v-b43b228e] { + white-space: nowrap; + overflow-wrap: normal; border: 1px solid var(--welcome-line); background: var(--welcome-tile-bg); min-height: 36px; diff --git a/tests/README.md b/tests/README.md index c5bece2..bccd27e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -38,7 +38,7 @@ CHROME_PATH=/absolute/path/to/chrome npm test - `review-purchase.e2e.test.mjs`:镜像订单未付复用、取消后重建、已付授权延迟不重复收费;级联候选与默认、结算、购物车恢复一致。 - `review-fields.e2e.test.mjs`:常见 ASCII 规则本地拦截、PCRE 专有规则交后端校验;实例随机密码实际提交及安全源失效、无偏采样边界。 - `review-theme.e2e.test.mjs`:认证页三态主题文案、持久化与跟随系统变化。 -- `review-style-layout.e2e.test.mjs`:维护条随内容与视口变化、超长通知内部滚动、帮助/购物车/机型/配置摘要吸顶避让,退出布局后的偏移清理与导航单一键盘焦点。 +- `review-style-layout.e2e.test.mjs`:维护条随内容与视口变化、超长通知内部滚动、帮助/购物车/机型/配置摘要吸顶避让,退出布局后的偏移清理与导航单一键盘焦点;首页用户 ID 复制按钮在各宽度下不拆行(该问题只在 Firefox 出现,场景额外启动 Playwright Firefox,需先执行 `npx playwright install firefox`)。 - `review-style-fields.e2e.test.mjs`:深色商品说明对比度、提现样式隔离、实名认证与转账凭证上传名称;实际升级弹窗的窄屏网格。 - `review-style-motion.e2e.test.mjs`:实例监控 Canvas 初始/刷新/加载反馈跟随系统减动偏好,并验证偏好切换及离页后无运行时错误。 - `review-online-auth.e2e.test.mjs`:真实密码输入的自动填充语义、键盘显隐且不误提交、浅暗主题按钮对比度;注册、找回、第三方绑定与账户新绑/改绑手机号固定 `+86`,保留旧号码真实区号和地址国家选项。登录发码与登录协议由原有认证场景共同覆盖。 diff --git a/tests/review-style-layout.e2e.test.mjs b/tests/review-style-layout.e2e.test.mjs index 8a05483..a8fbf77 100644 --- a/tests/review-style-layout.e2e.test.mjs +++ b/tests/review-style-layout.e2e.test.mjs @@ -4,6 +4,7 @@ */ import assert from "node:assert/strict"; import { test } from "node:test"; +import { firefox } from "playwright"; import { appOrigin, backendOrigin, @@ -479,3 +480,95 @@ test( return { navigationActions: 2, singleKeyboardStop: true }; }), ); + +// 首页用户 ID 按钮继承标题的任意断行时,Firefox 会把 “#” 和数字拆到两行;按钮必须作为整体排版。 +// Chromium 下该问题不出现,因此同一场景还要在 Firefox 中验证真实渲染。 +async function routeHomeAccount(context) { + await context.route(`${backendOrigin}/console/v1/index`, async (route) => { + if (route.request().method() !== "GET") return route.fallback(); + const response = await route.fetch(); + const body = await response.json(); + Object.assign(body.data.account, { id: 6, username: "测试用户" }); + return route.fulfill({ response, json: body }); + }); +} + +async function userIdChipGeometry(page) { + const sizes = []; + const chip = page.locator(".welcome-title .copy-id"); + await chip.waitFor(); + for (const width of [1440, 375, 320]) { + await page.setViewportSize({ width, height: 900 }); + const state = await eventually( + () => + chip.evaluate((button) => { + const text = [...button.childNodes].find( + (node) => node.nodeType === Node.TEXT_NODE, + ), + value = text.textContent, + glyph = (index) => { + const range = document.createRange(); + range.setStart(text, index); + range.setEnd(text, index + 1); + return range.getBoundingClientRect(); + }, + first = glyph(value.indexOf("#")), + last = glyph(value.length - 1), + box = button.getBoundingClientRect(), + lineHeight = parseFloat(getComputedStyle(button).lineHeight); + return { + label: value.trim(), + sameLine: Math.abs(first.top - last.top) <= 0.5, + singleLineHeight: box.height < lineHeight * 1.5, + }; + }), + (state) => state.sameLine && state.singleLineHeight, + "The user id must not wrap inside its copy button", + ); + assert.equal(state.label, "#6"); + sizes.push({ width, singleLine: true }); + } + return sizes; +} + +test( + "home user id chip stays on one line beside the greeting", + { timeout: 60000 }, + (t) => + scenario(t, {}, async (page) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + await page.setViewportSize({ width: 1440, height: 900 }); + await routeHomeAccount(page.context()); + await login(page, "/"); + const chromium = await userIdChipGeometry(page); + const browser = await firefox.launch(); + try { + const context = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + serviceWorkers: "block", + }); + // 与共享场景相同:Firefox 也只能访问两个本地 origin。 + const blocked = []; + await context.route("**/*", (route) => { + const url = new URL(route.request().url()); + if ( + !["http:", "https:"].includes(url.protocol) || + [appOrigin, backendOrigin].includes(url.origin) + ) + return route.fallback(); + blocked.push( + `${route.request().method()} ${url.origin}${url.pathname}`, + ); + return route.abort("blockedbyclient"); + }); + await routeHomeAccount(context); + const firefoxPage = await context.newPage(); + await login(firefoxPage, "/"); + const gecko = await userIdChipGeometry(firefoxPage); + assert.deepEqual(blocked, [], "Firefox must stay on local origins"); + return { chromium, firefox: gecko }; + } finally { + await browser.close(); + } + }), +);