修复:保留监控缺测语义、采样日期与磁盘字段名称
This commit is contained in:
1 parent
520c0894c2
commit
6a07904ca0
7 files changed
+726
-71
No files matched your search
@@ -0,0 +1,661 @@
|
||||
/**
|
||||
* 回归缺口:缺测值不能伪造为0,多日采样不能丢掉日期,磁盘输入的可见标签必须命名原生控件。
|
||||
* 只替换已配置的外部HTTP边界;经真实登录、实例标签、图表指针与磁盘表单完成验收。
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
backendOrigin,
|
||||
eventually,
|
||||
login,
|
||||
scenario,
|
||||
} from "./helpers/console-browser.mjs";
|
||||
|
||||
async function instanceBoundary(
|
||||
page,
|
||||
{ module = "mf_cloud", value = 50, time = 1793836800 } = {},
|
||||
) {
|
||||
const state = { value, quotes: [], chartReads: 0 };
|
||||
const times = [
|
||||
"2026-11-05 08:00:00",
|
||||
"2026-11-06 08:00:00",
|
||||
"2026-11-07 08:00:00",
|
||||
];
|
||||
const epoch = time;
|
||||
await page.context().route(`${backendOrigin}/console/v1/**`, (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const path = url.pathname.replace(/^\/console\/v1/, "");
|
||||
const reply = (data) =>
|
||||
route.fulfill({ json: { status: 200, msg: "", data } });
|
||||
if (path === "/host/5")
|
||||
return reply({
|
||||
host: {
|
||||
id: 5,
|
||||
product_id: 123,
|
||||
module,
|
||||
name: "本地指标验收实例",
|
||||
status: "Active",
|
||||
billing_cycle: "monthly",
|
||||
upgrade_allowed: true,
|
||||
},
|
||||
});
|
||||
if (path === "/host/5/view") return reply({ module });
|
||||
if (path === "/host/5/ip") return reply({ list: [] });
|
||||
if (path === "/idcsmart_common/host/5/configoption")
|
||||
return reply({
|
||||
configoptions: [],
|
||||
client_button: [],
|
||||
chart: [{ type: "bandwidth", title: "通用带宽监控", select: [] }],
|
||||
});
|
||||
if (path === "/idcsmart_common/host/5/provision/status")
|
||||
return reply({ status: "on" });
|
||||
if (path === "/idcsmart_common/host/5/configoption/chart") {
|
||||
state.chartReads++;
|
||||
return reply({
|
||||
unit: "Mbps",
|
||||
label: ["入带宽", "出带宽"],
|
||||
list: [
|
||||
times.map((time) => ({ time, value: state.value })),
|
||||
times.map((time) => ({ time, value: state.value })),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (path === `/${module}/5`)
|
||||
return reply({
|
||||
image: { id: 6, name: "Ubuntu 24.04" },
|
||||
port: 22,
|
||||
username: "root",
|
||||
cpu: 2,
|
||||
memory: 4,
|
||||
config: { reinstall_sms_verify: 0, reset_password_sms_verify: 0 },
|
||||
});
|
||||
if (path === `/${module}/5/status`)
|
||||
return reply({ status: "on", rescue: false });
|
||||
if (path === `/${module}/5/remote_info`)
|
||||
return reply({ username: "root", port: 22 });
|
||||
if (
|
||||
path === "/remf_finance/5/snapshot" ||
|
||||
path === "/remf_finance/5/backup"
|
||||
)
|
||||
return reply({ disk: [] });
|
||||
if (path === `/${module}/5/disk`)
|
||||
return reply({
|
||||
list: [
|
||||
{
|
||||
id: 10,
|
||||
name: "验收数据盘",
|
||||
size: 50,
|
||||
type: "SSD",
|
||||
type2: "data",
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (path === `/${module}/5/flow`)
|
||||
return reply({ total: 100, used: 20, leave: 80 });
|
||||
if (path === `/${module}/5/real_data`)
|
||||
return reply({ cpu_usage: state.value, memory_usage: state.value });
|
||||
if (path === `/${module}/5/chart`) {
|
||||
state.chartReads++;
|
||||
if (module === "remf_finance") {
|
||||
const type = url.searchParams.get("type");
|
||||
return reply({
|
||||
unit: type === "cpu" || type === "memory" ? "%" : "KB/s",
|
||||
label: [type === "memory" ? "已用" : "采样"],
|
||||
list: [times.map((time) => ({ time, value: state.value }))],
|
||||
});
|
||||
}
|
||||
return reply({
|
||||
list: [0, 1, 2].map((index) => ({
|
||||
time: epoch === null ? null : epoch + index * 86400,
|
||||
value: state.value,
|
||||
total: 100,
|
||||
used: state.value,
|
||||
in_bw: state.value,
|
||||
out_bw: state.value,
|
||||
read_bytes: state.value,
|
||||
write_bytes: state.value,
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (path === "/kky_ecs/disk/5") return reply({ list: [] });
|
||||
if (path === "/kky_ecs/5/traffic")
|
||||
return reply({
|
||||
billing: { mode: "bandwidth", monthly_quota: 0, unit_price: "0.00" },
|
||||
usage_status: "available",
|
||||
quota_status: "not_applicable",
|
||||
});
|
||||
if (path === "/kky_ecs/monitor/5") {
|
||||
state.chartReads++;
|
||||
return reply({
|
||||
sample_info: times.map((timestamp) => ({ timestamp })),
|
||||
cpu: [{ title: "CPU", value: times.map(() => state.value) }],
|
||||
memory: [
|
||||
{ title: "total", name: "total", value: [100, 100, 100] },
|
||||
{
|
||||
title: "free",
|
||||
name: "free",
|
||||
value: times.map(() =>
|
||||
typeof state.value === "number" ? 100 - state.value : state.value,
|
||||
),
|
||||
},
|
||||
],
|
||||
bandwidth: [{ title: "入", value: [50000, 50000, 50000] }],
|
||||
disk: [{ title: "读", value: times.map(() => state.value) }],
|
||||
});
|
||||
}
|
||||
if (path === `/product/123/${module}/order_page`)
|
||||
return reply({
|
||||
data_disk: [
|
||||
{
|
||||
type: "step",
|
||||
min_value: 10,
|
||||
max_value: 500,
|
||||
other_config: { disk_type: "SSD" },
|
||||
},
|
||||
],
|
||||
config: { disk_limit_num: 4 },
|
||||
});
|
||||
if (
|
||||
path === `/${module}/5/disk/resize` ||
|
||||
path === `/${module}/5/disk/price`
|
||||
) {
|
||||
state.quotes.push({
|
||||
path,
|
||||
body: request.postDataJSON(),
|
||||
authenticated: !!request.headers().authorization,
|
||||
});
|
||||
return reply({ price: "10.00" });
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
async function openMonitor(page) {
|
||||
await page.getByRole("tab", { name: "监控", exact: true }).click();
|
||||
await eventually(
|
||||
() => page.locator(".mon .chart-card").count(),
|
||||
(count) => count === 4,
|
||||
"The real monitor must finish mounting its four metric cards",
|
||||
);
|
||||
await eventually(
|
||||
() => page.getByRole("button", { name: "刷新", exact: true }).isEnabled(),
|
||||
Boolean,
|
||||
"The current HTTP refresh must finish",
|
||||
);
|
||||
}
|
||||
|
||||
async function hoverMeasurement(page, viewport, expectedValue) {
|
||||
await viewport.locator("canvas").waitFor();
|
||||
const box = await viewport.boundingBox();
|
||||
assert.ok(box, "The rendered chart must have a visible viewport");
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
return eventually(
|
||||
() => viewport.innerText(),
|
||||
(text) => text.includes(expectedValue),
|
||||
"A real pointer must expose the supplied measurement in the tooltip",
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
"overview distinguishes missing live measurements from an observed zero",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await instanceBoundary(page, { value: null });
|
||||
await login(page, "/servers/server?id=5");
|
||||
await eventually(
|
||||
async () =>
|
||||
(await page.locator(".ov .res .ring-value").count()) +
|
||||
(await page.locator(".ov .resource-unavailable").count()),
|
||||
(count) => count === 2,
|
||||
"Resource measurements must finish loading",
|
||||
);
|
||||
assert.equal(
|
||||
await page.locator(".ov .res .ring-value").count(),
|
||||
0,
|
||||
"Null is not a measured0%",
|
||||
);
|
||||
assert.equal(await page.locator(".ov .resource-unavailable").count(), 2);
|
||||
return { missingLiveMeasurements: 2, fabricatedZeroRings: 0 };
|
||||
}),
|
||||
);
|
||||
|
||||
test(
|
||||
"monitor keeps null blank and malformed samples unavailable instead of drawing zero",
|
||||
{ timeout: 45000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
const state = await instanceBoundary(page, { value: null });
|
||||
await login(page, "/servers/server?id=5");
|
||||
await openMonitor(page);
|
||||
const observations = [];
|
||||
for (const value of [null, "", " ", "invalid", false]) {
|
||||
if (observations.length) {
|
||||
state.value = value;
|
||||
const before = state.chartReads;
|
||||
await page.getByRole("button", { name: "刷新", exact: true }).click();
|
||||
await eventually(
|
||||
() => state.chartReads,
|
||||
(count) => count === before + 4,
|
||||
"Refresh must read every metric throughHTTP",
|
||||
);
|
||||
await eventually(
|
||||
() =>
|
||||
page
|
||||
.getByRole("button", { name: "刷新", exact: true })
|
||||
.isEnabled(),
|
||||
Boolean,
|
||||
"Current measurement request must settle",
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
await page.locator(".mon .chart-viewport canvas").count(),
|
||||
0,
|
||||
"Missing or malformed values must not become a real measurement curve",
|
||||
);
|
||||
assert.equal(await page.locator(".mon .chart-empty").count(), 4);
|
||||
observations.push({ input: value, unavailableCharts: 4 });
|
||||
}
|
||||
return { cases: observations };
|
||||
}),
|
||||
);
|
||||
|
||||
for (const module of ["mf_cloud", "remf_finance", "kky_ecs"]) {
|
||||
test(
|
||||
`real zero ${module} measurements remain visible in overview and monitor`,
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await instanceBoundary(page, { module, value: 0 });
|
||||
await login(page, "/servers/server?id=5");
|
||||
await eventually(
|
||||
() => page.locator(".ov .res .ring-value").allTextContents(),
|
||||
(values) =>
|
||||
values.length === 2 && values.every((value) => value === "0%"),
|
||||
"Actual zero must remain an observed resource value",
|
||||
);
|
||||
await openMonitor(page);
|
||||
await eventually(
|
||||
() => page.locator(".mon .chart-viewport canvas").count(),
|
||||
(count) => count === 4,
|
||||
"Observed zero measurements must finish drawing every metric chart",
|
||||
);
|
||||
const tooltip = await hoverMeasurement(
|
||||
page,
|
||||
page.locator(".mon .chart-viewport").first(),
|
||||
"0%",
|
||||
);
|
||||
return { module, observedZeroRings: 2, observedZeroCharts: 4, tooltip };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (const module of ["mf_cloud", "remf_finance", "kky_ecs"]) {
|
||||
test(
|
||||
`multi-day ${module} monitoring tooltip identifies the sample date`,
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await instanceBoundary(page, { module });
|
||||
await login(page, "/servers/server?id=5");
|
||||
const overviewTooltip = await hoverMeasurement(
|
||||
page,
|
||||
page.locator(".ov .network-chart-viewport"),
|
||||
"50",
|
||||
);
|
||||
assert.match(overviewTooltip, /2026[-/年]11[-/月]0?6/);
|
||||
await openMonitor(page);
|
||||
if (module !== "kky_ecs") {
|
||||
await page.getByRole("button", { name: "7天", exact: true }).click();
|
||||
await eventually(
|
||||
() =>
|
||||
page
|
||||
.getByRole("button", { name: "刷新", exact: true })
|
||||
.isEnabled(),
|
||||
Boolean,
|
||||
"Weekly range must finish reading",
|
||||
);
|
||||
}
|
||||
const tooltips = [];
|
||||
for (const viewport of await page
|
||||
.locator(".mon .chart-viewport")
|
||||
.all()) {
|
||||
const tooltip = await hoverMeasurement(page, viewport, "50");
|
||||
assert.match(
|
||||
tooltip,
|
||||
/2026[-/年]11[-/月]0?6/,
|
||||
"Equal HH:mm samples on different days require the actual date in the tooltip",
|
||||
);
|
||||
assert.match(tooltip, /08:00/);
|
||||
tooltips.push(tooltip);
|
||||
}
|
||||
return {
|
||||
module,
|
||||
suppliedMiddleDate: "2026-11-06",
|
||||
overviewTooltip,
|
||||
tooltips,
|
||||
dateIdentifiesSample: true,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
"KKY overview keeps an absent CPU and free-memory sample unavailable",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await instanceBoundary(page, { module: "kky_ecs", value: null });
|
||||
await login(page, "/servers/server?id=5");
|
||||
await eventually(
|
||||
() => page.locator(".ov .resource-unavailable").count(),
|
||||
(count) => count === 2,
|
||||
"Missing CPU and partial memory history cannot fabricate usage",
|
||||
);
|
||||
assert.equal(await page.locator(".ov .res .ring-value").count(), 0);
|
||||
return { module: "kky_ecs", missingHistoricalMeasurements: 2 };
|
||||
}),
|
||||
);
|
||||
|
||||
test(
|
||||
"KKY overview keeps a successful empty history unavailable",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await instanceBoundary(page, { module: "kky_ecs" });
|
||||
await page
|
||||
.context()
|
||||
.route(`${backendOrigin}/console/v1/kky_ecs/monitor/5`, (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
status: 200,
|
||||
msg: "",
|
||||
data: { cpu: [], memory: [], bandwidth: [], sample_info: [] },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await login(page, "/servers/server?id=5");
|
||||
await eventually(
|
||||
() => page.locator(".ov .resource-unavailable").count(),
|
||||
(count) => count === 2,
|
||||
"An empty measurement history is not two observed zero values",
|
||||
);
|
||||
assert.equal(await page.locator(".ov .res .ring-value").count(), 0);
|
||||
return {
|
||||
module: "kky_ecs",
|
||||
emptyHistory: true,
|
||||
unavailableMeasurements: 2,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
test(
|
||||
"physical-server monitoring tooltip retains the actual sample date",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await instanceBoundary(page, { module: "mf_dcim" });
|
||||
await login(page, "/servers/server?id=5");
|
||||
await page.getByRole("tab", { name: "监控", exact: true }).click();
|
||||
await page.getByRole("button", { name: "7 天", exact: true }).click();
|
||||
const viewport = page.locator(".monitor:not(.compact) .chart-viewport");
|
||||
await eventually(
|
||||
() => viewport.getAttribute("aria-busy"),
|
||||
(busy) => busy === "false",
|
||||
"Physical-server sampling request must settle",
|
||||
);
|
||||
const tooltip = await hoverMeasurement(page, viewport, "50");
|
||||
assert.match(
|
||||
tooltip,
|
||||
/2026[-/年]11[-/月]0?6/,
|
||||
"Physical-server tooltip must identify the day as well as the time",
|
||||
);
|
||||
assert.match(tooltip, /08:00/);
|
||||
return { module: "mf_dcim", tooltip, dateIdentifiesSample: true };
|
||||
}),
|
||||
);
|
||||
|
||||
for (const input of [" ", false]) {
|
||||
test(
|
||||
`physical-server monitoring rejects non-measurement ${JSON.stringify(input)}`,
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await instanceBoundary(page, { module: "mf_dcim", value: input });
|
||||
await login(page, "/servers/server?id=5");
|
||||
await page.getByRole("tab", { name: "监控", exact: true }).click();
|
||||
const viewport = page.locator(".monitor:not(.compact) .chart-viewport");
|
||||
await eventually(
|
||||
() => viewport.getAttribute("aria-busy"),
|
||||
(busy) => busy === "false",
|
||||
"Physical-server sampling request must settle",
|
||||
);
|
||||
assert.equal(
|
||||
await viewport.locator("canvas").count(),
|
||||
0,
|
||||
"Whitespace and booleans are not bandwidth samples",
|
||||
);
|
||||
assert.equal(
|
||||
await viewport
|
||||
.getByText("所选时段暂无带宽采样", { exact: true })
|
||||
.count(),
|
||||
1,
|
||||
);
|
||||
return { input, unavailableBandwidthChart: true };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
"physical-server monitoring does not turn absent sample times into 1970",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await instanceBoundary(page, { module: "mf_dcim", time: null });
|
||||
await login(page, "/servers/server?id=5");
|
||||
await page.getByRole("tab", { name: "监控", exact: true }).click();
|
||||
const viewport = page.locator(".monitor:not(.compact) .chart-viewport");
|
||||
await eventually(
|
||||
() => viewport.getAttribute("aria-busy"),
|
||||
(busy) => busy === "false",
|
||||
"Physical-server sampling request must settle",
|
||||
);
|
||||
assert.equal(
|
||||
await viewport.locator("canvas").count(),
|
||||
0,
|
||||
"A value without a sample time must not become a 1970 measurement",
|
||||
);
|
||||
assert.equal(
|
||||
await viewport
|
||||
.getByText("所选时段暂无带宽采样", { exact: true })
|
||||
.count(),
|
||||
1,
|
||||
);
|
||||
return { missingTimestamp: null, fabricatedEpochChart: false };
|
||||
}),
|
||||
);
|
||||
|
||||
test(
|
||||
"generic-instance charts distinguish unavailable samples from a measured zero stroke",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
const state = await instanceBoundary(page, {
|
||||
module: "idcsmart_common",
|
||||
value: 0,
|
||||
});
|
||||
await login(page, "/servers/server?id=5");
|
||||
await page.getByRole("button", { name: "监控", exact: true }).click();
|
||||
const chart = page.locator(".common-chart");
|
||||
await chart.locator("canvas").waitFor();
|
||||
// richText 提示也绘制在 Canvas 内;验收实际图像中的数据线,不读取 ECharts 私有实例或 option。
|
||||
const measurementPixels = () =>
|
||||
chart
|
||||
.locator("canvas")
|
||||
.first()
|
||||
.evaluate((canvas) => {
|
||||
const copy = document.createElement("canvas");
|
||||
copy.width = canvas.width;
|
||||
copy.height = canvas.height;
|
||||
const context = copy.getContext("2d");
|
||||
context.drawImage(canvas, 0, 0);
|
||||
const { data } = context.getImageData(
|
||||
0,
|
||||
0,
|
||||
copy.width,
|
||||
copy.height,
|
||||
);
|
||||
const scale = copy.width / canvas.clientWidth;
|
||||
let colored = 0;
|
||||
for (
|
||||
let y = Math.ceil(48 * scale);
|
||||
y < copy.height - 40 * scale;
|
||||
y++
|
||||
) {
|
||||
for (
|
||||
let x = Math.ceil(60 * scale);
|
||||
x < copy.width - 25 * scale;
|
||||
x++
|
||||
) {
|
||||
const index = (y * copy.width + x) * 4;
|
||||
const channels = [
|
||||
data[index],
|
||||
data[index + 1],
|
||||
data[index + 2],
|
||||
];
|
||||
if (
|
||||
data[index + 3] > 80 &&
|
||||
Math.max(...channels) - Math.min(...channels) > 40
|
||||
)
|
||||
colored++;
|
||||
}
|
||||
}
|
||||
return colored;
|
||||
});
|
||||
await eventually(
|
||||
measurementPixels,
|
||||
(pixels) => pixels > 100,
|
||||
"Measured zero must draw a real colored series, not an empty chart",
|
||||
);
|
||||
const cases = [];
|
||||
for (const value of [" ", false, null, "invalid"]) {
|
||||
state.value = value;
|
||||
const before = state.chartReads;
|
||||
await page.getByRole("button", { name: "刷新", exact: true }).click();
|
||||
await eventually(
|
||||
() => state.chartReads,
|
||||
(count) => count === before + 1,
|
||||
"Generic monitoring refresh must use the configured HTTP boundary",
|
||||
);
|
||||
await eventually(
|
||||
() =>
|
||||
page.getByRole("button", { name: "刷新", exact: true }).isEnabled(),
|
||||
Boolean,
|
||||
"Generic monitoring refresh must settle",
|
||||
);
|
||||
await eventually(
|
||||
measurementPixels,
|
||||
(pixels) => pixels === 0,
|
||||
"Unavailable input must not leave a fabricated colored zero curve",
|
||||
);
|
||||
cases.push({ input: value, hasMeasurementStroke: false });
|
||||
}
|
||||
return {
|
||||
module: "idcsmart_common",
|
||||
measuredZeroHasStroke: true,
|
||||
unavailableCases: cases,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
test(
|
||||
"disk resize visible label names the editable capacity and preserves quote inputs",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
const state = await instanceBoundary(page);
|
||||
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.getByRole("spinbutton").waitFor();
|
||||
assert.equal(
|
||||
await dialog
|
||||
.getByRole("spinbutton", { name: "扩容至", exact: true })
|
||||
.count(),
|
||||
1,
|
||||
"Visible resize label must name the native spinbutton",
|
||||
);
|
||||
const field = dialog.getByRole("spinbutton", {
|
||||
name: "扩容至",
|
||||
exact: true,
|
||||
});
|
||||
await field.fill("80");
|
||||
await field.press("Tab");
|
||||
await eventually(
|
||||
() => state.quotes.at(-1)?.body.resize_data_disk?.[0]?.size,
|
||||
(size) => size === 80,
|
||||
"Keyboard edit must reach the real resize quote",
|
||||
);
|
||||
assert.equal(state.quotes.at(-1).authenticated, true);
|
||||
return {
|
||||
accessibleName: "扩容至",
|
||||
quotedCapacityGb: 80,
|
||||
authenticatedQuote: true,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
test(
|
||||
"new disk capacity visible label names its editable field",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
const state = await instanceBoundary(page);
|
||||
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.getByRole("spinbutton").waitFor();
|
||||
assert.equal(
|
||||
await dialog
|
||||
.getByRole("spinbutton", { name: "容量", exact: true })
|
||||
.count(),
|
||||
1,
|
||||
"Visible add-capacity label must name the native spinbutton",
|
||||
);
|
||||
const field = dialog.getByRole("spinbutton", {
|
||||
name: "容量",
|
||||
exact: true,
|
||||
});
|
||||
await field.fill("40");
|
||||
await field.press("Tab");
|
||||
await eventually(
|
||||
() => state.quotes.at(-1)?.body.add_disk?.[0]?.size,
|
||||
(size) => size === 40,
|
||||
"Keyboard edit must reach the real add-disk quote",
|
||||
);
|
||||
assert.equal(state.quotes.at(-1).authenticated, true);
|
||||
return {
|
||||
accessibleName: "容量",
|
||||
quotedCapacityGb: 40,
|
||||
authenticatedQuote: true,
|
||||
};
|
||||
}),
|
||||
);
|
||||
Reference in new issue
Block a user