- 禁止用户 ID 在按钮内拆行,确保长用户名场景下按钮整体换行 - 添加 Chromium 和 Firefox 在不同视口宽度下的布局回归测试 - 更新 CI 浏览器安装配置及测试文档 - 忽略包含生产服务器结构信息的本地运维技能目录
This commit is contained in:
5 files changed
+101
-2
No files matched your search
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: 安装锁定依赖
|
- name: 安装锁定依赖
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: 安装测试浏览器
|
- name: 安装测试浏览器
|
||||||
run: npx playwright install --with-deps chromium
|
run: npx playwright install --with-deps chromium firefox
|
||||||
- name: 检查源码并构建
|
- name: 检查源码并构建
|
||||||
run: |
|
run: |
|
||||||
npm run check
|
npm run check
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ test-results/
|
|||||||
|
|
||||||
# 本地交接、审计报告和验收截图不随源码交付;README 保留项目使用说明。
|
# 本地交接、审计报告和验收截图不随源码交付;README 保留项目使用说明。
|
||||||
docs/
|
docs/
|
||||||
|
|
||||||
|
# 本地运维 skill 含生产服务器结构说明,只留在操作者本机。
|
||||||
|
.claude/skills/crashwork-ops/
|
||||||
@@ -123,7 +123,10 @@
|
|||||||
.welcome-body[data-v-b43b228e] > .p-message {
|
.welcome-body[data-v-b43b228e] > .p-message {
|
||||||
margin: 0 26px 22px;
|
margin: 0 26px 22px;
|
||||||
}
|
}
|
||||||
|
/* 用户 ID 是不可拆分的整体:不继承标题的任意断行,长用户名时整个按钮换到下一行。 */
|
||||||
.copy-id[data-v-b43b228e] {
|
.copy-id[data-v-b43b228e] {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-wrap: normal;
|
||||||
border: 1px solid var(--welcome-line);
|
border: 1px solid var(--welcome-line);
|
||||||
background: var(--welcome-tile-bg);
|
background: var(--welcome-tile-bg);
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ CHROME_PATH=/absolute/path/to/chrome npm test
|
|||||||
- `review-purchase.e2e.test.mjs`:镜像订单未付复用、取消后重建、已付授权延迟不重复收费;级联候选与默认、结算、购物车恢复一致。
|
- `review-purchase.e2e.test.mjs`:镜像订单未付复用、取消后重建、已付授权延迟不重复收费;级联候选与默认、结算、购物车恢复一致。
|
||||||
- `review-fields.e2e.test.mjs`:常见 ASCII 规则本地拦截、PCRE 专有规则交后端校验;实例随机密码实际提交及安全源失效、无偏采样边界。
|
- `review-fields.e2e.test.mjs`:常见 ASCII 规则本地拦截、PCRE 专有规则交后端校验;实例随机密码实际提交及安全源失效、无偏采样边界。
|
||||||
- `review-theme.e2e.test.mjs`:认证页三态主题文案、持久化与跟随系统变化。
|
- `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-fields.e2e.test.mjs`:深色商品说明对比度、提现样式隔离、实名认证与转账凭证上传名称;实际升级弹窗的窄屏网格。
|
||||||
- `review-style-motion.e2e.test.mjs`:实例监控 Canvas 初始/刷新/加载反馈跟随系统减动偏好,并验证偏好切换及离页后无运行时错误。
|
- `review-style-motion.e2e.test.mjs`:实例监控 Canvas 初始/刷新/加载反馈跟随系统减动偏好,并验证偏好切换及离页后无运行时错误。
|
||||||
- `review-online-auth.e2e.test.mjs`:真实密码输入的自动填充语义、键盘显隐且不误提交、浅暗主题按钮对比度;注册、找回、第三方绑定与账户新绑/改绑手机号固定 `+86`,保留旧号码真实区号和地址国家选项。登录发码与登录协议由原有认证场景共同覆盖。
|
- `review-online-auth.e2e.test.mjs`:真实密码输入的自动填充语义、键盘显隐且不误提交、浅暗主题按钮对比度;注册、找回、第三方绑定与账户新绑/改绑手机号固定 `+86`,保留旧号码真实区号和地址国家选项。登录发码与登录协议由原有认证场景共同覆盖。
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
|
import { firefox } from "playwright";
|
||||||
import {
|
import {
|
||||||
appOrigin,
|
appOrigin,
|
||||||
backendOrigin,
|
backendOrigin,
|
||||||
@@ -479,3 +480,95 @@ test(
|
|||||||
return { navigationActions: 2, singleKeyboardStop: true };
|
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();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in new issue
Block a user