修复深色说明样式归属与实名认证上传字段标签
This commit is contained in:
1 parent
56188874cb
commit
21821ec6fa
4 files changed
+383
-17
No files matched your search
@@ -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 };
|
||||
}),
|
||||
);
|
||||
Reference in new issue
Block a user