修复深色说明样式归属与实名认证上传字段标签

This commit is contained in:
cieanyau committed 2026-11-06 03:12:03 +08:00
1 parent 56188874cb
commit 21821ec6fa
4 files changed
+383 -17

No files matched your search

+354
View File
@@ -0,0 +1,354 @@
/** 主题和字段回归经真实商品、首页、实名认证与实例入口验证;只替换外部 HTTP 响应。 */
import assert from "node:assert/strict";
import test from "node:test";
import { backendOrigin, login, scenario } from "./helpers/console-browser.mjs";
async function boundary(page, responses) {
const requests = [];
await page
.context()
.route(`${backendOrigin}/console/v1/**`, async (route) => {
const request = route.request();
const path = new URL(request.url()).pathname.replace(
/^\/console\/v1/,
"",
);
if (!Object.hasOwn(responses, path)) return route.fallback();
requests.push({
path,
authenticated: Boolean(request.headers().authorization),
multipart:
request
.headers()
["content-type"]?.startsWith("multipart/form-data; boundary=") ===
true,
});
await route.fulfill({
json: { status: 200, msg: "", data: responses[path] },
headers: { "Access-Control-Allow-Origin": "*" },
});
});
return requests;
}
async function textPresentation(locator) {
return locator.evaluate((element) => {
const style = getComputedStyle(element);
const rgb = (value) => value.match(/[\d.]+/g).map(Number);
let background = [255, 255, 255];
const layers = [];
for (let owner = element; owner; owner = owner.parentElement)
layers.push(rgb(getComputedStyle(owner).backgroundColor));
for (const layer of layers.reverse()) {
const alpha = layer[3] ?? 1;
background = background.map(
(channel, index) => channel * (1 - alpha) + layer[index] * alpha,
);
}
const foreground = rgb(style.color);
const luminance = (color) =>
color
.slice(0, 3)
.map((channel) => {
const value = channel / 255;
return value <= 0.04045
? value / 12.92
: ((value + 0.055) / 1.055) ** 2.4;
})
.reduce(
(total, value, index) =>
total + value * [0.2126, 0.7152, 0.0722][index],
0,
);
const light = luminance(foreground);
const dark = luminance(background);
const tokenColor = new Option().style;
tokenColor.color = style.getPropertyValue("--p-text-color");
return {
color: style.color,
textColor: tokenColor.color,
background: background.map(Math.round),
contrast: (Math.max(light, dark) + 0.05) / (Math.min(light, dark) + 0.05),
};
});
}
test(
"dark structured catalog descriptions keep readable field text",
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
await page.emulateMedia({ colorScheme: "dark" });
await boundary(page, {
"/product/group/first": { list: [{ id: 1, name: "云产品" }] },
"/product/group/second": {
list: [{ id: 2, parent_id: 1, name: "测试分类", description: "" }],
},
"/product": {
list: [
{
id: 123,
name: "布局验收商品",
module: "idcsmart_common",
res_module: "idcsmart_common",
pay_type: "recurring_prepayment",
price: "10.00",
billing_cycle: "monthly",
stock_control: 0,
qty: 10,
order_allowed: true,
description: "配置:4GB 内存\n线路:BGP",
},
],
count: 1,
},
});
await login(page, "/products?g=1&sg=2");
const value = page
.locator(".description-field dd")
.filter({ hasText: "4GB" });
const label = page
.locator(".description-field dt")
.filter({ hasText: "配置" });
await value.waitFor();
await label.waitFor();
assert.ok(
(await textPresentation(value)).contrast >= 4.5,
"Structured description values must remain readable in dark mode",
);
assert.ok(
(await textPresentation(label)).contrast >= 4.5,
"Structured description labels must remain readable in dark mode",
);
return { structuredDescriptionReadable: true };
}),
);
test(
"dark withdrawal notices do not recolor the home announcement title",
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
await page.emulateMedia({ colorScheme: "dark" });
await boundary(page, {
"/announcement": {
list: [{ id: 1, title: "本地公告", create_time: 1700000000 }],
count: 1,
},
"/withdraw/rule/credit": {
status: 1,
method: [{ id: 0, name: "账户余额", no_account: true }],
min: "1.00",
max: "100.00",
withdraw_fee_type: "fixed",
withdraw_fee: "0.00",
},
});
await login(page, "/");
const announcement = page
.locator(".home-row-title")
.filter({ hasText: "本地公告" });
await announcement.waitFor();
const home = await textPresentation(announcement);
assert.equal(
home.color,
home.textColor,
"The announcement title owns its normal text color",
);
await page.getByRole("button", { name: "提现", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "提现", exact: true });
const notice = dialog.locator(".notice-title");
await notice.waitFor();
const ownNotice = await textPresentation(notice);
assert.ok(
ownNotice.contrast >= 4.5,
"Withdrawal instructions must remain readable",
);
assert.notEqual(
ownNotice.color,
home.color,
"The withdrawal notice retains its own emphasis color",
);
assert.equal((await textPresentation(announcement)).color, home.color);
return {
homeAnnouncementColorOwned: true,
withdrawalNoticeReadable: true,
};
}),
);
test(
"real-name upload fields expose distinct labels and named removal actions",
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
const requests = await boundary(page, {
"/certification/info": {
certification_open: 1,
certification_company_open: 1,
certification_upload: 1,
certification_company_need_person: 0,
is_certification: 0,
person: { status: 0 },
company: { status: 0 },
},
"/upload": { save_name: "identity-front.png" },
});
await login(page, "/account/realname/personal");
const front = page.getByLabel("身份证正面照 *", { exact: true });
const back = page.getByLabel("身份证反面照 *", { exact: true });
await front.waitFor();
await back.waitFor();
assert.notEqual(
await front.getAttribute("id"),
await back.getAttribute("id"),
);
const file = {
name: "identity-front.png",
mimeType: "image/png",
buffer: Buffer.from("local-e2e-upload-not-identity-data"),
};
await front.setInputFiles(file);
const remove = page.getByRole("button", {
name: "移除身份证正面照 *",
exact: true,
});
await remove.waitFor();
assert.deepEqual(
requests.filter((request) => request.path === "/upload"),
[{ path: "/upload", authenticated: true, multipart: true }],
);
await remove.click();
assert.equal(await remove.count(), 0);
await front.setInputFiles(file);
await remove.waitFor();
return {
separateFileInputNames: true,
namedRemoval: true,
sameFileReselection: true,
authenticatedMultipartUpload: true,
};
}),
);
test(
"normal kky upgrade plans and disks fit the narrow dialog container",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await boundary(page, {
"/host/5": {
host: {
id: 5,
product_id: 123,
module: "kky_ecs",
name: "本地弹性云",
status: "Active",
billing_cycle: "monthly",
upgrade_allowed: true,
renew_amount: "10.00",
},
},
"/kky_ecs/5": {
cpu: 2,
memory: 4,
bw: 10,
protect: 0,
ip: "192.0.2.5",
username: "root",
port: 22,
image: { name: "Ubuntu 24.04" },
},
"/kky_ecs/5/status": { status: "on", rescue: false },
"/kky_ecs/5/traffic": {
mode: "bandwidth",
monthly_quota: 100,
traffic_show: 0,
},
"/kky_ecs/disk/5": {
list: [
{
id: "data-1",
name: "数据盘",
size: 40,
type2: "data",
fs_type: "SSD",
},
],
},
"/kky_ecs/monitor/5": {
sample_info: [],
cpu: [],
memory: [],
bandwidth: [],
disk: [],
},
"/host/5/renew/auto": { status: 0 },
"/refund": { allow_refund: 0, reasons: [] },
"/refund/host/5/refund": { refund: null },
"/promo_code/host/5/promo_code": { promo_code: [] },
"/kky_ecs/upgrade/5/ins_upgrade_config": [
{
cpu_num: 4,
mem_list: [
{ id: 1, memory: 8, is_sell_out: 0, description: "常规升级规格" },
],
},
],
"/kky_ecs/upgrade/5/add_disk_config": {
min: 10,
max: 1000,
step: 10,
type: ["SSD"],
remaining_num: 0,
},
});
await login(page, "/servers/server?id=5");
await page.getByRole("tab", { name: "计费", exact: true }).click();
await page.getByRole("button", { name: "升降级", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "升降级", exact: true });
await dialog.locator(".plans .plan").waitFor();
const widths = [];
const assertFits = async (gridName, cardName) => {
const measured = await dialog
.locator(gridName)
.evaluate((grid, childSelector) => {
const bounds = grid.getBoundingClientRect();
const card = grid
.querySelector(childSelector)
.getBoundingClientRect();
return {
width: bounds.width,
scroll: grid.scrollWidth,
client: grid.clientWidth,
rightOverflow: card.right - bounds.right,
};
}, cardName);
assert.ok(measured.width > 0);
assert.ok(
measured.rightOverflow <= 1 && measured.scroll <= measured.client + 1,
`${gridName} must shrink to its actual dialog container: ${JSON.stringify(measured)}`,
);
widths.push({
viewport: page.viewportSize().width,
grid: gridName,
width: Math.round(measured.width),
fits: true,
});
};
for (const width of [375, 320]) {
await page.setViewportSize({ width, height: 900 });
await assertFits(".plans", ".plan");
}
await dialog.getByRole("button", { name: /硬盘/ }).click();
await dialog
.getByRole("button", { name: "扩容已有盘", exact: true })
.click();
await dialog.locator(".disks .disk").waitFor();
for (const width of [375, 320]) {
await page.setViewportSize({ width, height: 900 });
await assertFits(".disks", ".disk");
}
return { grids: widths };
}),
);