修复:保留监控缺测语义、采样日期与磁盘字段名称
This commit is contained in:
1 parent
520c0894c2
commit
6a07904ca0
7 files changed
+726
-71
No files matched your search
@@ -32,6 +32,7 @@ import { r as configuration_display_r } from "~/lib/configuration-display.js";
|
|||||||
import { t as configuration_display_t } from "~/lib/configuration-display.js";
|
import { t as configuration_display_t } from "~/lib/configuration-display.js";
|
||||||
import { t as common_advanced_rules_t } from "~/lib/common-advanced-rules.js";
|
import { t as common_advanced_rules_t } from "~/lib/common-advanced-rules.js";
|
||||||
import multiselect from "openvue/multiselect";
|
import multiselect from "openvue/multiselect";
|
||||||
|
import { t as finance_resource_chart_t } from "~/lib/finance-resource-chart.js";
|
||||||
var j = (e) => (e && typeof e == `object` && !Array.isArray(e) ? e : {}),
|
var j = (e) => (e && typeof e == `object` && !Array.isArray(e) ? e : {}),
|
||||||
M = (e) => e === !0 || e === 1 || e === `1`,
|
M = (e) => e === !0 || e === 1 || e === `1`,
|
||||||
N = (e) => e === !1 || e === 0 || e === `0`,
|
N = (e) => e === !1 || e === 0 || e === `0`,
|
||||||
@@ -163,12 +164,7 @@ function z(e) {
|
|||||||
name: a[t] || `数据 ${t + 1}`,
|
name: a[t] || `数据 ${t + 1}`,
|
||||||
type: `line`,
|
type: `line`,
|
||||||
showSymbol: !1,
|
showSymbol: !1,
|
||||||
data: c.map((e) => {
|
data: c.map((e) => finance_resource_chart_t(n.get(e))),
|
||||||
let t = n.get(e);
|
|
||||||
return t != null && t !== `` && Number.isFinite(Number(t))
|
|
||||||
? Number(t)
|
|
||||||
: null;
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import button from "openvue/button";
|
|||||||
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
|
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
|
||||||
import skeleton from "openvue/skeleton";
|
import skeleton from "openvue/skeleton";
|
||||||
import selectbutton from "openvue/selectbutton";
|
import selectbutton from "openvue/selectbutton";
|
||||||
|
import { r as dict_r } from "~/lib/dict.js";
|
||||||
|
import { t as finance_resource_chart_t } from "~/lib/finance-resource-chart.js";
|
||||||
var T = defineAsyncComponent(() =>
|
var T = defineAsyncComponent(() =>
|
||||||
loadModule(
|
loadModule(
|
||||||
() => import("~/components/ChartBox.vue").then((e) => e.default || e),
|
() => import("~/components/ChartBox.vue").then((e) => e.default || e),
|
||||||
@@ -113,8 +115,10 @@ var T = defineAsyncComponent(() =>
|
|||||||
start_time: Math.floor(Date.now() / 1e3) - S.value,
|
start_time: Math.floor(Date.now() / 1e3) - S.value,
|
||||||
});
|
});
|
||||||
if (R || e !== L || t !== h.hostId || n !== h.module) return;
|
if (R || e !== L || t !== h.hostId || n !== h.module) return;
|
||||||
((F.value = (Array.isArray(r.list) ? r.list : []).filter((e) =>
|
((F.value = (Array.isArray(r.list) ? r.list : []).filter(
|
||||||
Number.isFinite(Number(e.time)),
|
(e) =>
|
||||||
|
finance_resource_chart_t(e.time) !== null &&
|
||||||
|
Number.isFinite(new Date(Number(e.time) * 1e3).getTime()),
|
||||||
)),
|
)),
|
||||||
(I.value = typeof r.unit == `string` ? r.unit : ``));
|
(I.value = typeof r.unit == `string` ? r.unit : ``));
|
||||||
} catch (t) {
|
} catch (t) {
|
||||||
@@ -126,12 +130,8 @@ var T = defineAsyncComponent(() =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 缺失采样转为 null 而不是 0,配合 connectNulls:false 保留采集中断的空隙。
|
// 缺失采样转为 null 而不是 0,配合 connectNulls:false 保留采集中断的空隙。
|
||||||
function V(e) {
|
let V = finance_resource_chart_t,
|
||||||
return e != null && e !== `` && Number.isFinite(Number(e))
|
H = computed(() =>
|
||||||
? Number(e)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
let H = computed(() =>
|
|
||||||
F.value.some((e) => V(e.in_bw) !== null || V(e.out_bw) !== null),
|
F.value.some((e) => V(e.in_bw) !== null || V(e.out_bw) !== null),
|
||||||
),
|
),
|
||||||
U = computed(() => ({
|
U = computed(() => ({
|
||||||
@@ -150,12 +150,10 @@ var T = defineAsyncComponent(() =>
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: `category`,
|
type: `category`,
|
||||||
boundaryGap: !1,
|
boundaryGap: !1,
|
||||||
data: F.value.map((e) =>
|
data: F.value.map((e) => dict_r(Number(e.time))),
|
||||||
new Date(Number(e.time) * 1e3).toLocaleTimeString(`zh-CN`, {
|
axisLabel: {
|
||||||
hour: `2-digit`,
|
formatter: (e) => String(e).slice(11, 16),
|
||||||
minute: `2-digit`,
|
},
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: `value`,
|
type: `value`,
|
||||||
|
|||||||
@@ -1055,13 +1055,14 @@ var ge = [`aria-busy`],
|
|||||||
createElementVNode(`div`, ke, [
|
createElementVNode(`div`, ke, [
|
||||||
(n[17] ||= createElementVNode(
|
(n[17] ||= createElementVNode(
|
||||||
`label`,
|
`label`,
|
||||||
null,
|
{ for: `disk-resize-capacity` },
|
||||||
`扩容至`,
|
`扩容至`,
|
||||||
-1,
|
-1,
|
||||||
)),
|
)),
|
||||||
createVNode(
|
createVNode(
|
||||||
fe,
|
fe,
|
||||||
{
|
{
|
||||||
|
inputId: `disk-resize-capacity`,
|
||||||
modelValue: unref(P),
|
modelValue: unref(P),
|
||||||
"onUpdate:modelValue": [
|
"onUpdate:modelValue": [
|
||||||
(n[1] ||= (e) =>
|
(n[1] ||= (e) =>
|
||||||
@@ -1232,13 +1233,14 @@ var ge = [`aria-busy`],
|
|||||||
createElementVNode(`div`, Fe, [
|
createElementVNode(`div`, Fe, [
|
||||||
(n[21] ||= createElementVNode(
|
(n[21] ||= createElementVNode(
|
||||||
`label`,
|
`label`,
|
||||||
null,
|
{ for: `disk-add-capacity` },
|
||||||
`容量`,
|
`容量`,
|
||||||
-1,
|
-1,
|
||||||
)),
|
)),
|
||||||
createVNode(
|
createVNode(
|
||||||
fe,
|
fe,
|
||||||
{
|
{
|
||||||
|
inputId: `disk-add-capacity`,
|
||||||
modelValue: unref(B),
|
modelValue: unref(B),
|
||||||
"onUpdate:modelValue": [
|
"onUpdate:modelValue": [
|
||||||
(n[5] ||= (e) =>
|
(n[5] ||= (e) =>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import ListSegments from "~/components/ListSegments.vue";
|
|||||||
import skeleton from "openvue/skeleton";
|
import skeleton from "openvue/skeleton";
|
||||||
import { n as finance_resource_chart_n } from "~/lib/finance-resource-chart.js";
|
import { n as finance_resource_chart_n } from "~/lib/finance-resource-chart.js";
|
||||||
import { t as finance_resource_chart_t } from "~/lib/finance-resource-chart.js";
|
import { t as finance_resource_chart_t } from "~/lib/finance-resource-chart.js";
|
||||||
|
import { r as dict_r } from "~/lib/dict.js";
|
||||||
var O = defineAsyncComponent(() =>
|
var O = defineAsyncComponent(() =>
|
||||||
loadModule(
|
loadModule(
|
||||||
() => import("~/components/ChartBox.vue").then((e) => e.default || e),
|
() => import("~/components/ChartBox.vue").then((e) => e.default || e),
|
||||||
@@ -125,8 +126,10 @@ var O = defineAsyncComponent(() =>
|
|||||||
],
|
],
|
||||||
Z = ref(86400),
|
Z = ref(86400),
|
||||||
Q = (e) => {
|
Q = (e) => {
|
||||||
let t = new Date(e * 1e3);
|
let t = finance_resource_chart_t(e);
|
||||||
return `${String(t.getHours()).padStart(2, `0`)}:${String(t.getMinutes()).padStart(2, `0`)}`;
|
return t !== null && Number.isFinite(new Date(t * 1e3).getTime())
|
||||||
|
? dict_r(t)
|
||||||
|
: ``;
|
||||||
};
|
};
|
||||||
async function te(e, t, n = `mf_cloud`) {
|
async function te(e, t, n = `mf_cloud`) {
|
||||||
// 云模块各指标并行读取并分别展示失败;物理智简只请求实际支持的带宽图。
|
// 云模块各指标并行读取并分别展示失败;物理智简只请求实际支持的带宽图。
|
||||||
@@ -151,10 +154,13 @@ var O = defineAsyncComponent(() =>
|
|||||||
pick: [
|
pick: [
|
||||||
{
|
{
|
||||||
name: `内存`,
|
name: `内存`,
|
||||||
fn: (e) =>
|
fn: (e) => {
|
||||||
e.total
|
let t = finance_resource_chart_t(e.total),
|
||||||
? (Number(e.used ?? 0) / Number(e.total)) * 100
|
n = finance_resource_chart_t(e.used);
|
||||||
: void 0,
|
return t !== null && t > 0 && n !== null
|
||||||
|
? (n / t) * 100
|
||||||
|
: null;
|
||||||
|
},
|
||||||
color: U.value,
|
color: U.value,
|
||||||
unit: `%`,
|
unit: `%`,
|
||||||
},
|
},
|
||||||
@@ -228,10 +234,7 @@ var O = defineAsyncComponent(() =>
|
|||||||
};
|
};
|
||||||
let r = n.value.list ?? [],
|
let r = n.value.list ?? [],
|
||||||
a = e.pick.map((e) =>
|
a = e.pick.map((e) =>
|
||||||
r.map((t) => {
|
r.map((t) => finance_resource_chart_t(e.fn(t))),
|
||||||
let n = e.fn(t);
|
|
||||||
return n === void 0 || Number.isNaN(n) ? null : Number(n);
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
s =
|
s =
|
||||||
e.scale === `bits`
|
e.scale === `bits`
|
||||||
@@ -317,9 +320,7 @@ var O = defineAsyncComponent(() =>
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let a = (r.list?.[0] ?? []).map((e) =>
|
let a = (r.list?.[0] ?? []).map((e) => String(e.time ?? ``)),
|
||||||
String(e.time ?? ``).slice(11, 16),
|
|
||||||
),
|
|
||||||
o = (r.list ?? []).map((e, t) => ({
|
o = (r.list ?? []).map((e, t) => ({
|
||||||
name: String(r.label?.[t] ?? `系列 ${t + 1}`).replace(
|
name: String(r.label?.[t] ?? `系列 ${t + 1}`).replace(
|
||||||
/\(.*\)$/,
|
/\(.*\)$/,
|
||||||
@@ -341,9 +342,7 @@ var O = defineAsyncComponent(() =>
|
|||||||
async function re(e) {
|
async function re(e) {
|
||||||
// 快快云监控时间范围由接口决定,带宽值按其既有口径从千分单位转换为 Mbps。
|
// 快快云监控时间范围由接口决定,带宽值按其既有口径从千分单位转换为 Mbps。
|
||||||
let t = await y.get(`/kky_ecs/monitor/${e}`),
|
let t = await y.get(`/kky_ecs/monitor/${e}`),
|
||||||
n = (t.sample_info ?? []).map((e) =>
|
n = (t.sample_info ?? []).map((e) => String(e.timestamp ?? ``)),
|
||||||
String(e.timestamp ?? ``).slice(11, 16),
|
|
||||||
),
|
|
||||||
r = (e, t, r, i, a, o = 1) => ({
|
r = (e, t, r, i, a, o = 1) => ({
|
||||||
key: e,
|
key: e,
|
||||||
title: t,
|
title: t,
|
||||||
@@ -352,9 +351,10 @@ var O = defineAsyncComponent(() =>
|
|||||||
name: e.title,
|
name: e.title,
|
||||||
unit: a,
|
unit: a,
|
||||||
color: i[t % i.length],
|
color: i[t % i.length],
|
||||||
data: (e.value ?? []).map((e) =>
|
data: (e.value ?? []).map((e) => {
|
||||||
typeof e == `number` ? Number((e / o).toFixed(2)) : null,
|
let t = finance_resource_chart_t(e);
|
||||||
),
|
return t === null ? null : Number((t / o).toFixed(2));
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
return [
|
return [
|
||||||
@@ -448,6 +448,7 @@ var O = defineAsyncComponent(() =>
|
|||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: t,
|
color: t,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
formatter: (e) => String(e).slice(11, 16),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import FlowPacketPanel from "~/components/instance/FlowPacketPanel.vue";
|
|||||||
import InstanceStatusTag from "~/components/instance/InstanceStatusTag.vue";
|
import InstanceStatusTag from "~/components/instance/InstanceStatusTag.vue";
|
||||||
import InstanceTraffic from "~/components/instance/InstanceTraffic.vue";
|
import InstanceTraffic from "~/components/instance/InstanceTraffic.vue";
|
||||||
import { n as finance_resource_chart_n } from "~/lib/finance-resource-chart.js";
|
import { n as finance_resource_chart_n } from "~/lib/finance-resource-chart.js";
|
||||||
|
import { t as finance_resource_chart_t } from "~/lib/finance-resource-chart.js";
|
||||||
var le = defineAsyncComponent(() =>
|
var le = defineAsyncComponent(() =>
|
||||||
loadModule(
|
loadModule(
|
||||||
() => import("~/components/ChartBox.vue").then((e) => e.default || e),
|
() => import("~/components/ChartBox.vue").then((e) => e.default || e),
|
||||||
@@ -293,13 +294,8 @@ var le = defineAsyncComponent(() =>
|
|||||||
),
|
),
|
||||||
K = 0,
|
K = 0,
|
||||||
q = !1;
|
q = !1;
|
||||||
function J(e) {
|
let J = finance_resource_chart_t,
|
||||||
if (typeof e != `number` && (typeof e != `string` || !e.trim()))
|
xt = (e) => (e.status === `fulfilled` ? e.value : null),
|
||||||
return null;
|
|
||||||
let t = Number(e);
|
|
||||||
return Number.isFinite(t) ? t : null;
|
|
||||||
}
|
|
||||||
let xt = (e) => (e.status === `fulfilled` ? e.value : null),
|
|
||||||
St = (e) => (e instanceof Error ? e.message : `网络监控加载失败`);
|
St = (e) => (e instanceof Error ? e.message : `网络监控加载失败`);
|
||||||
async function Y() {
|
async function Y() {
|
||||||
// 不同模块的监控时间单位和返回结构不同,分支完成协议转换;实例切换后拒绝旧响应。
|
// 不同模块的监控时间单位和返回结构不同,分支完成协议转换;实例切换后拒绝旧响应。
|
||||||
@@ -374,7 +370,7 @@ var le = defineAsyncComponent(() =>
|
|||||||
(e) => {
|
(e) => {
|
||||||
H.value = {
|
H.value = {
|
||||||
labels: (e?.list?.[0] ?? []).map((e) =>
|
labels: (e?.list?.[0] ?? []).map((e) =>
|
||||||
String(e.time ?? ``).slice(11, 16),
|
String(e.time ?? ``),
|
||||||
),
|
),
|
||||||
unit: e?.unit?.trim() || ``,
|
unit: e?.unit?.trim() || ``,
|
||||||
series: (e?.list ?? []).map((t, n) => ({
|
series: (e?.list ?? []).map((t, n) => ({
|
||||||
@@ -474,8 +470,11 @@ var le = defineAsyncComponent(() =>
|
|||||||
: 0;
|
: 0;
|
||||||
H.value = {
|
H.value = {
|
||||||
labels: t.map((e) => {
|
labels: t.map((e) => {
|
||||||
let t = new Date(e.time * 1e3);
|
let t = J(e.time);
|
||||||
return `${String(t.getHours()).padStart(2, `0`)}:${String(t.getMinutes()).padStart(2, `0`)}`;
|
return t !== null &&
|
||||||
|
Number.isFinite(new Date(t * 1e3).getTime())
|
||||||
|
? dict_r(t)
|
||||||
|
: ``;
|
||||||
}),
|
}),
|
||||||
unit: [`bps`, `Kbps`, `Mbps`, `Gbps`, `Tbps`][i],
|
unit: [`bps`, `Kbps`, `Mbps`, `Gbps`, `Tbps`][i],
|
||||||
series: n.map((e, t) => ({
|
series: n.map((e, t) => ({
|
||||||
@@ -498,32 +497,34 @@ var le = defineAsyncComponent(() =>
|
|||||||
]);
|
]);
|
||||||
if (!r()) return;
|
if (!r()) return;
|
||||||
let n = xt(e);
|
let n = xt(e);
|
||||||
z.value = n?.cpu?.[0]?.value ?? [];
|
z.value = (n?.cpu?.[0]?.value ?? []).map(J);
|
||||||
let i = n?.memory?.find((e) => !/free/i.test(e.name)),
|
let i = n?.memory?.find((e) => !/free/i.test(e.name)),
|
||||||
a = n?.memory?.find((e) => /free/i.test(e.name));
|
a = n?.memory?.find((e) => /free/i.test(e.name));
|
||||||
((B.value =
|
((B.value =
|
||||||
i && a
|
i && a
|
||||||
? i.value.map((e, t) => {
|
? i.value.map((e, t) => {
|
||||||
let n = Number(a.value?.[t] ?? 0);
|
let n = J(a.value?.[t]),
|
||||||
return e > 0
|
r = J(e);
|
||||||
? Math.max(0, Math.min(100, ((e - n) / e) * 100))
|
return r !== null && r > 0 && n !== null
|
||||||
: 0;
|
? Math.max(0, Math.min(100, ((r - n) / r) * 100))
|
||||||
|
: null;
|
||||||
})
|
})
|
||||||
: []),
|
: []),
|
||||||
e.status === `rejected`
|
e.status === `rejected`
|
||||||
? (W.value = St(e.reason))
|
? (W.value = St(e.reason))
|
||||||
: (H.value = {
|
: (H.value = {
|
||||||
labels: (n?.sample_info ?? []).map((e) =>
|
labels: (n?.sample_info ?? []).map((e) =>
|
||||||
String(e.timestamp ?? ``).slice(11, 16),
|
String(e.timestamp ?? ``),
|
||||||
),
|
),
|
||||||
unit: `Mbps`,
|
unit: `Mbps`,
|
||||||
series: (n?.bandwidth ?? []).map((e, t) => ({
|
series: (n?.bandwidth ?? []).map((e, t) => ({
|
||||||
name: e.title || e.name || `带宽系列 ${t + 1}`,
|
name: e.title || e.name || `带宽系列 ${t + 1}`,
|
||||||
data: (e.value ?? []).map((e) =>
|
data: (e.value ?? []).map((e) => {
|
||||||
typeof e == `number` && Number.isFinite(e)
|
let t = J(e);
|
||||||
? Number((e / 1e3).toFixed(2))
|
return t === null
|
||||||
: null,
|
? null
|
||||||
),
|
: Number((t / 1e3).toFixed(2));
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -546,23 +547,19 @@ var le = defineAsyncComponent(() =>
|
|||||||
x.module === `jy_cloud` || x.module === `jy_dcim`
|
x.module === `jy_cloud` || x.module === `jy_dcim`
|
||||||
? (J(R.value?.cpu_usage) ?? z.value.at(-1) ?? null)
|
? (J(R.value?.cpu_usage) ?? z.value.at(-1) ?? null)
|
||||||
: x.module === `mf_cloud` || x.module === `remf_cloud`
|
: x.module === `mf_cloud` || x.module === `remf_cloud`
|
||||||
? X(R.value?.cpu_usage)
|
? J(R.value?.cpu_usage)
|
||||||
: z.value.length
|
: z.value.length
|
||||||
? z.value[z.value.length - 1]
|
? z.value[z.value.length - 1]
|
||||||
: x.module === `remf_finance`
|
: null,
|
||||||
? null
|
|
||||||
: 0,
|
|
||||||
),
|
),
|
||||||
wt = computed(() =>
|
wt = computed(() =>
|
||||||
x.module === `jy_cloud` || x.module === `jy_dcim`
|
x.module === `jy_cloud` || x.module === `jy_dcim`
|
||||||
? (J(R.value?.memory_usage) ?? B.value.at(-1) ?? null)
|
? (J(R.value?.memory_usage) ?? B.value.at(-1) ?? null)
|
||||||
: x.module === `mf_cloud` || x.module === `remf_cloud`
|
: x.module === `mf_cloud` || x.module === `remf_cloud`
|
||||||
? X(R.value?.memory_usage)
|
? J(R.value?.memory_usage)
|
||||||
: B.value.length
|
: B.value.length
|
||||||
? B.value[B.value.length - 1]
|
? B.value[B.value.length - 1]
|
||||||
: x.module === `remf_finance`
|
: null,
|
||||||
? null
|
|
||||||
: 0,
|
|
||||||
);
|
);
|
||||||
function Tt(e) {
|
function Tt(e) {
|
||||||
// CPU/内存趋势以百分比绘制;null 表示缺测,禁止连接缺失点伪造连续数据。
|
// CPU/内存趋势以百分比绘制;null 表示缺测,禁止连接缺失点伪造连续数据。
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function t(t, n) {
|
|||||||
? null
|
? null
|
||||||
: Number(Math.min(100, Math.max(0, n)).toFixed(2));
|
: Number(Math.min(100, Math.max(0, n)).toFixed(2));
|
||||||
},
|
},
|
||||||
s = (e) => e.map((e) => String(e.time ?? ``).slice(11, 16));
|
s = (e) => e.map((e) => String(e.time ?? ``));
|
||||||
if (n === `cpu` || /%|percent/i.test(t.unit ?? ``)) {
|
if (n === `cpu` || /%|percent/i.test(t.unit ?? ``)) {
|
||||||
let e = r[n === `memory` && a >= 0 ? a : 0] ?? [];
|
let e = r[n === `memory` && a >= 0 ? a : 0] ?? [];
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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