推进 INI 面板 run readiness workflow helper

This commit is contained in:
2026-06-15 05:21:01 +08:00
parent a883af2d3e
commit 4a307f63f6
8 changed files with 322 additions and 11 deletions

View File

@@ -371,6 +371,87 @@ host_wasm_opfs_browser_smokes=ok
`loadMachineSessionIntoWasmWorkflow()`,避免 restore+load 与单独 load 的 field/log 状态模型
分叉;仍保持由 LinuxCNC/OPFS bridge 执行实际语义,不在 JS 中解释 CNC 行为。
三、2026-06-15 继续执行记录INI panel run readiness workflow helper
本轮按“第一原则:加快实质性推进”继续推进 run 前置状态模型,将 run 按钮可执行条件、
默认/快照 G-code 选择结果与已加载 session 的 WASM 绑定关系抽成可复用 readiness helper
让 app、state summary、control view 和 Node smoke 共享同一份运行前状态。
完成内容:
- 新增 `wasm-port/runtime/ui/ini-panel/run-workflow.js` 的
`createRunSessionReadinessState(input)`
- `runGcodeSessionWorkflow()` 现在先生成 readiness state再复用其中的选定程序和 loaded
session 信息执行实际 run
- readiness state 固定返回:
- `phase`
- `canRun`
- `missing`
- `loadedSession`
- `snapshot`
- `defaultProgram`
- `program`
- `runButton`
- `wasm-port/runtime/ui/ini-panel/app.js` 在点击 Run 前先写入 readiness state并把
`selectedProgram`、`runReadiness` 和 field state 一起更新到 panel state
- `wasm-port/runtime/ui/ini-panel/panel-state-summary.js` 新增 `runReadiness` 的 report /
workflowStatus / control-view 映射;
- `createMachineSessionControlView()` 的 Run 区新增 `Ready` 与 `Button` 行,用于 read-only
control page 读取首帧 run 前置状态;
- `wasm-port/tests/ui/node/verify_ini_panel_run_workflow.mjs` 覆盖 readiness helper 与
workflow readiness 返回值;
- `wasm-port/tests/ui/node/verify_ini_panel_state_summary.mjs` 覆盖 runReadiness 在 report、
bundle、workflowStatus 和 control view 中的映射;
- `wasm-port/docs/ui-run-summary-helpers.md` 和 `wasm-port/docs/ui-panel-state-summary.md`
同步说明 readiness helper 与 runReadiness 状态;
- 未新增控制按钮;
- 未解析 G-code
- 未解释 canonical events
- 未改变 OPFS/session persistence、LinuxCNC interpreter、tool、parameter、planner 或
LinuxCNC-owned runtime semantics。
验证已通过:
```bash
git diff --check
wasm-port/tests/ui/node/verify_ini_panel_run_workflow.sh
wasm-port/tests/ui/node/verify_ini_panel_state_summary.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tools/verify_vendor_sync.sh
wasm-port/tools/verify_no_standalone_cnc_semantics.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh
wasm-port/tests/host/verify_host_smokes.sh
```
关键输出:
```text
ini_panel_run_workflow_node_smoke=ok
ini_panel_state_summary_node_smoke=ok
ui_node_smokes=ok
browser_ini_opfs_smoke=ok
browser_ini_control_page_smoke=ok
browser_ini_launch_smoke=ok
vendor sync up to date
standalone CNC semantics guard complete
interp_wasm_node_smoke=ok
sim_configs_wasm_node_inventory_executed=28
sim_configs_wasm_node_inventory_passed=28
sim_configs_wasm_node_inventory_skipped=131
sim_configs_wasm_node_inventory_unexpected_fail=0
browser_interp_smoke=ok
host_wasm_opfs_browser_smokes=ok
```
下一步建议:
继续推进 OPFS/session persistence 的实质链路。下一批建议将 `load-session` 与
`restore-load-session` 的 field/log 路径再做一次轻量整合,优先把“已加载 session、快照恢复、
run readiness、run 结果”汇总成一个更上层的只读工作流状态对象;仍保持只描述 UI/host
boundary facts不在 JS 中解释 G-code、canonical events 或 CNC 运行语义。
三、2026-06-15 继续执行记录INI panel restore-load workflow 复用 load-session helper
本轮按“第一原则:加快实质性推进”继续推进 OPFS/session persistence 主链路,将

View File

@@ -49,6 +49,7 @@ For the short page/entry map, see `docs/panel-entry.md`.
machineFiles: { iniOpfsPath: "...", toolTableOpfsPath: "...", parameterOpfsPath: "...", gcodeOpfsPath: "..." },
sessionSnapshot: { snapshotLabel: "...", workflow: "save-session-snapshot" },
sessionLoad: { iniOpfsPath: "...", iniWasmPath: "..." },
runReadiness: { phase: "ready", canRun: true, runButton: { enabled: true } },
run: { runStatus: "ok", programSource: "snapshot" },
fiveaxisRemap: "ok"
}
@@ -100,6 +101,7 @@ with four sections:
session: { snapshotLabel: "...", workflow: "...", ini: "...", parameters: "...", toolTable: "...", gcode: "..." },
loaded: { iniWasmPath: "...", parameterWasmPath: "...", toolTableWasmPath: "..." },
program: { source: "snapshot", opfsPath: "...", wasmPath: "..." },
runReadiness: { phase: "ready", canRun: true, buttonEnabled: true, programOpfsPath: "..." },
run: { status: "ok", canonicalEventCount: 2, motionSnapshotCount: 2, iniWasmPath: "..." },
status: { lastAction: "run-gcode", error: null, fiveaxisRemap: "ok" }
}
@@ -130,6 +132,7 @@ This helper derives a lightweight status object for upper-level control pages:
```js
{
readiness: { ini: "...", interp: "...", opfs: "..." },
runReadiness: { phase: "ready", canRun: true, buttonEnabled: true },
lastAction: "run-gcode",
error: null,
session: { snapshotLabel: "...", workflow: "...", gcode: "..." },
@@ -167,7 +170,7 @@ This helper maps a bundle into UI-ready status and sections:
sections: [
{ id: "readiness", title: "Readiness", rows: [{ label: "INI", value: "..." }] },
{ id: "session", title: "Session", rows: [{ label: "Workflow", value: "..." }] },
{ id: "run", title: "Run", rows: [{ label: "Canonical events", value: 2 }] }
{ id: "run", title: "Run", rows: [{ label: "Ready", value: "ready" }] }
]
}
```

View File

@@ -7,7 +7,7 @@ summarizing an INI panel G-code run.
`runtime/ui/ini-panel/run-workflow.js` contains the reusable UI workflow for
selecting the session/default G-code program, loading it from OPFS, staging it
into the interpreter WASM filesystem, invoking the LinuxCNC interpreter, and
returning run field/log state. These helpers are intentionally pure so browser
returning run readiness, field, and log state. These helpers are intentionally pure so browser
UI panels, SDK-facing wrappers, and Node smoke tests can share the same summary
shape without reading DOM text or parsing log output.
@@ -95,8 +95,8 @@ functions:
```
It returns the selected program mapping, raw interpreter result text, run
summary, UI field state, log lines, and optional UI motion snapshots produced
by the injected `summarizeRun` callback:
summary, readiness state, UI field state, log lines, and optional UI motion
snapshots produced by the injected `summarizeRun` callback:
```js
{
@@ -108,6 +108,7 @@ by the injected `summarizeRun` callback:
},
resultText: "canon_event=...",
summary: { runStatus: "ok", programSource: "snapshot" },
readiness: { phase: "ready", canRun: true },
fields: { runStatus: "ok", sessionGcode: "linuxcnc/gcode/ui-session.ngc" },
motionSnapshots: [{ line: 1 }],
logLines: ["Program: linuxcnc/gcode/ui-session.ngc -> /work/ui-session.ngc"]
@@ -117,6 +118,10 @@ by the injected `summarizeRun` callback:
`selectSessionGcodeProgram(input)` is the pure program-selection helper used by
the workflow. Invalid snapshot G-code paths fall back to the UI default program.
`createRunSessionReadinessState(input)` is the pure readiness helper used by the
workflow and panel state summary. It reports the selected program, loaded
session path, snapshot metadata, and whether the run button can execute.
## Boundary Rules
- Do not parse G-code text in these helpers.

View File

@@ -32,6 +32,7 @@ import {
summarizeRunResult,
} from "./run-summary.js";
import {
createRunSessionReadinessState,
runGcodeSessionWorkflow,
} from "./run-workflow.js";
import {
@@ -772,8 +773,28 @@ document.getElementById("restore-load-session").addEventListener("click", async
document.getElementById("run-gcode").addEventListener("click", async () => {
try {
if (!loadedSession) {
throw new Error("Load a machine session before running G-code.");
const readiness = createRunSessionReadinessState({
loadedSession,
snapshot: restoredSessionSnapshot,
defaultFilename: UI_SESSION.defaultGcodeFilename,
defaultOpfsPath: defaultGcodeOpfsPath(),
wasmPath: UI_SESSION.gcodeWasmPath,
gcodeFilenameFromProgramPath,
});
updatePanelMachineSessionState({
runReadiness: readiness,
selectedProgram: readiness.program,
fields: {
sessionGcode: readiness.program.opfsPath,
runSource: readiness.program.source,
runOpfsPath: readiness.program.opfsPath,
runWasmPath: readiness.program.wasmPath,
},
lastAction: "run-gcode-readiness",
error: readiness.runButton.reason,
});
if (!readiness.canRun) {
throw new Error(readiness.runButton.reason ?? "Load a machine session before running G-code.");
}
const interp = requireInterpSdk();
clearRunPlayback();
@@ -803,6 +824,7 @@ document.getElementById("run-gcode").addEventListener("click", async () => {
setField("runStatus", workflow.fields.runStatus);
setLastRunSummary(workflow.summary);
updatePanelMachineSessionState({
runReadiness: workflow.readiness,
selectedProgram: {
source: workflow.program.source,
opfsPath: workflow.program.opfsPath,

View File

@@ -4,6 +4,7 @@ export function createIniPanelStateSummary({
machineFiles = null,
sessionSnapshot = null,
sessionLoad = null,
runReadiness = null,
run = null,
fiveaxisRemap = null,
} = {}) {
@@ -17,6 +18,7 @@ export function createIniPanelStateSummary({
machineFiles,
sessionSnapshot,
sessionLoad,
runReadiness,
run,
fiveaxisRemap: fiveaxisRemap ?? null,
};
@@ -28,6 +30,7 @@ export function createMachineSessionStateSnapshot({
machineFiles = null,
sessionSnapshot = null,
sessionLoad = null,
runReadiness = null,
selectedProgram = null,
run = null,
fields = {},
@@ -42,6 +45,7 @@ export function createMachineSessionStateSnapshot({
machineFiles,
sessionSnapshot,
sessionLoad,
runReadiness,
run,
fiveaxisRemap,
}),
@@ -69,6 +73,7 @@ export function createMachineSessionStateReport(state = {}) {
const fields = state.fields ?? {};
const sessionSnapshot = state.sessionSnapshot ?? {};
const sessionLoad = state.sessionLoad ?? {};
const runReadiness = state.runReadiness ?? {};
const selectedProgram = state.selectedProgram ?? {};
const run = state.run ?? {};
return {
@@ -95,6 +100,17 @@ export function createMachineSessionStateReport(state = {}) {
opfsPath: selectedProgram.opfsPath ?? run.programOpfsPath ?? fields.runOpfsPath ?? null,
wasmPath: selectedProgram.wasmPath ?? run.programWasmPath ?? fields.runWasmPath ?? null,
},
runReadiness: {
phase: runReadiness.phase ?? null,
canRun: Boolean(runReadiness.canRun),
missing: Array.isArray(runReadiness.missing) ? runReadiness.missing : [],
buttonEnabled: Boolean(runReadiness.runButton?.enabled),
buttonReason: runReadiness.runButton?.reason ?? null,
programSource: runReadiness.program?.source ?? null,
programOpfsPath: runReadiness.program?.opfsPath ?? null,
programWasmPath: runReadiness.program?.wasmPath ?? null,
iniWasmPath: runReadiness.loadedSession?.iniWasmPath ?? null,
},
run: {
status: run.runStatus ?? fields.runStatus ?? null,
canonicalEventCount: run.canonicalEventCount ?? 0,
@@ -123,6 +139,7 @@ export function createMachineSessionWorkflowStatus(state = {}) {
const report = createMachineSessionStateReport(state);
return {
readiness: report.readiness,
runReadiness: report.runReadiness,
lastAction: report.status.lastAction,
error: report.status.error,
session: report.session,
@@ -145,6 +162,7 @@ export function createMachineSessionControlView(bundle = {}) {
const report = bundle.report ?? {};
const workflowStatus = bundle.workflowStatus ?? {};
const session = workflowStatus.session ?? report.session ?? {};
const runReadiness = workflowStatus.runReadiness ?? report.runReadiness ?? {};
const run = workflowStatus.run ?? report.run ?? {};
return {
status: {
@@ -176,6 +194,8 @@ export function createMachineSessionControlView(bundle = {}) {
id: "run",
title: "Run",
rows: [
{ label: "Ready", value: runReadiness.phase ?? null },
{ label: "Button", value: runReadiness.buttonEnabled ? "enabled" : "disabled" },
{ label: "Status", value: run.status ?? null },
{ label: "Canonical events", value: run.canonicalEventCount ?? 0 },
{ label: "Motion snapshots", value: run.motionSnapshotCount ?? 0 },

View File

@@ -34,6 +34,55 @@ export function selectSessionGcodeProgram({
};
}
export function createRunSessionReadinessState({
loadedSession = null,
snapshot = null,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
}) {
const program = selectSessionGcodeProgram({
snapshot,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
});
const missing = [];
if (!loadedSession?.ini?.wasmPath) {
missing.push("loaded-session");
}
if (!program.filename || !program.opfsPath || !program.wasmPath) {
missing.push("gcode-program");
}
const canRun = missing.length === 0;
return {
phase: canRun ? "ready" : "blocked",
canRun,
missing,
loadedSession: {
iniWasmPath: loadedSession?.ini?.wasmPath ?? null,
},
snapshot: {
present: Boolean(snapshot),
label: snapshot?.metadata?.snapshotLabel ?? snapshot?.sessionId ?? null,
workflow: snapshot?.metadata?.workflow ?? null,
gcodeOpfsPath: snapshot?.payload?.files?.gcode ?? null,
},
defaultProgram: {
filename: defaultFilename ?? null,
opfsPath: defaultOpfsPath ?? null,
wasmPath: wasmPath ?? null,
},
program,
runButton: {
enabled: canRun,
reason: canRun ? null : `Missing ${missing.join(", ")}.`,
},
};
}
export function runSessionContextLogLines(program, snapshot = null, snapshotLabel = null) {
const summary = createSessionSnapshotSummary({
metadata: snapshot?.metadata ?? {},
@@ -58,16 +107,18 @@ export async function runGcodeSessionWorkflow({
summarizeRun = () => ({}),
snapshotLabel = null,
}) {
if (!loadedSession) {
throw new Error("Load a machine session before running G-code.");
}
const program = selectSessionGcodeProgram({
const readiness = createRunSessionReadinessState({
loadedSession,
snapshot,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
});
if (!readiness.canRun) {
throw new Error("Load a machine session before running G-code.");
}
const program = readiness.program;
const programText = await loadGcodeProgram(program.filename);
interp.writeTextFile(program.wasmPath, programText);
const resultText = interp.runFileWithIni(program.wasmPath, loadedSession.ini.wasmPath).trim();
@@ -91,6 +142,7 @@ export async function runGcodeSessionWorkflow({
resultText,
motionSnapshots,
summary,
readiness,
fields: {
sessionGcode: program.opfsPath,
runStatus: summary.runStatus,

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import {
createRunSessionReadinessState,
runGcodeSessionWorkflow,
selectSessionGcodeProgram,
} from "../../../runtime/ui/ini-panel/run-workflow.js";
@@ -42,6 +43,61 @@ assert.deepEqual(snapshotProgram, {
wasmPath: "/work/ui-session.ngc",
});
const readiness = createRunSessionReadinessState({
loadedSession: {
ini: {
wasmPath: "/work/session-machine.ini",
},
},
snapshot: {
sessionId: "ui-machine-session",
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
gcode: "linuxcnc/gcode/snapshot-session.ngc",
},
},
},
defaultFilename: "ui-session.ngc",
defaultOpfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
gcodeFilenameFromProgramPath: (path) => path.split("/").at(-1),
});
assert.deepEqual(readiness, {
phase: "ready",
canRun: true,
missing: [],
loadedSession: {
iniWasmPath: "/work/session-machine.ini",
},
snapshot: {
present: true,
label: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
gcodeOpfsPath: "linuxcnc/gcode/snapshot-session.ngc",
},
defaultProgram: {
filename: "ui-session.ngc",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
program: {
filename: "snapshot-session.ngc",
opfsPath: "linuxcnc/gcode/snapshot-session.ngc",
source: "snapshot",
wasmPath: "/work/ui-session.ngc",
},
runButton: {
enabled: true,
reason: null,
},
});
const calls = [];
const workflow = await runGcodeSessionWorkflow({
interp: {
@@ -126,6 +182,7 @@ assert.deepEqual(workflow.fields, {
runOpfsPath: "linuxcnc/gcode/snapshot-session.ngc",
runWasmPath: "/work/ui-session.ngc",
});
assert.deepEqual(workflow.readiness, readiness);
assert.deepEqual(workflow.motionSnapshots, [{ line: 1 }]);

View File

@@ -13,6 +13,60 @@ import {
renderMachineSessionControlView,
} from "../../../runtime/ui/ini-panel/control-view-renderer.js";
const runReadinessFixture = {
phase: "ready",
canRun: true,
missing: [],
loadedSession: {
iniWasmPath: "/work/session-machine.ini",
},
snapshot: {
present: true,
label: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
defaultProgram: {
filename: "ui-session.ngc",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
program: {
filename: "ui-session.ngc",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
source: "default",
wasmPath: "/work/ui-session.ngc",
},
runButton: {
enabled: true,
reason: null,
},
};
const expectedRunReadinessReport = {
phase: "ready",
canRun: true,
missing: [],
buttonEnabled: true,
buttonReason: null,
programSource: "default",
programOpfsPath: "linuxcnc/gcode/ui-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: "/work/session-machine.ini",
};
const expectedEmptyRunReadinessReport = {
phase: null,
canRun: false,
missing: [],
buttonEnabled: false,
buttonReason: null,
programSource: null,
programOpfsPath: null,
programWasmPath: null,
iniWasmPath: null,
};
const summary = createIniPanelStateSummary({
badges: {
ini: "INI WASM: ready",
@@ -37,6 +91,7 @@ const summary = createIniPanelStateSummary({
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
iniWasmPath: "/work/session-machine.ini",
},
runReadiness: runReadinessFixture,
run: {
runStatus: "ok",
programSource: "snapshot",
@@ -68,6 +123,7 @@ assert.deepEqual(summary, {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
iniWasmPath: "/work/session-machine.ini",
},
runReadiness: runReadinessFixture,
run: {
runStatus: "ok",
programSource: "snapshot",
@@ -87,6 +143,7 @@ assert.deepEqual(
machineFiles: null,
sessionSnapshot: null,
sessionLoad: null,
runReadiness: null,
run: null,
fiveaxisRemap: null,
},
@@ -108,6 +165,7 @@ assert.deepEqual(
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
},
runReadiness: runReadinessFixture,
selectedProgram: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
@@ -149,6 +207,7 @@ assert.deepEqual(
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
},
runReadiness: runReadinessFixture,
run: {
runStatus: "ok",
programSource: "snapshot",
@@ -219,6 +278,7 @@ assert.deepEqual(
parameterWasmPath: "/work/session-linuxcnc.var",
toolTableWasmPath: "/work/session-tool.tbl",
},
runReadiness: runReadinessFixture,
selectedProgram: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
@@ -261,6 +321,7 @@ assert.deepEqual(
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
runReadiness: expectedRunReadinessReport,
run: {
status: "ok",
canonicalEventCount: 2,
@@ -299,6 +360,7 @@ const expectedExportReport = {
opfsPath: null,
wasmPath: null,
},
runReadiness: expectedEmptyRunReadinessReport,
run: {
status: null,
canonicalEventCount: 0,
@@ -350,6 +412,7 @@ assert.deepEqual(
runStatus: "ok",
runWorkflow: "save-session-snapshot",
},
runReadiness: runReadinessFixture,
run: {
runStatus: "ok",
canonicalEventCount: 2,
@@ -364,6 +427,7 @@ assert.deepEqual(
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
runReadiness: expectedRunReadinessReport,
lastAction: "run-gcode",
error: null,
session: {
@@ -407,6 +471,7 @@ const expectedBundleReport = {
opfsPath: null,
wasmPath: null,
},
runReadiness: expectedRunReadinessReport,
run: {
status: "ok",
canonicalEventCount: 2,
@@ -434,6 +499,7 @@ assert.deepEqual(
runStatus: "ok",
runWorkflow: "save-session-snapshot",
},
runReadiness: runReadinessFixture,
run: {
runStatus: "ok",
canonicalEventCount: 2,
@@ -458,6 +524,7 @@ assert.deepEqual(
},
lastAction: "run-gcode",
error: null,
runReadiness: expectedRunReadinessReport,
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
@@ -501,6 +568,7 @@ assert.deepEqual(
toolTable: null,
gcode: "linuxcnc/gcode/ui-session.ngc",
},
runReadiness: expectedRunReadinessReport,
run: {
status: "ok",
canonicalEventCount: 2,
@@ -539,6 +607,8 @@ assert.deepEqual(
id: "run",
title: "Run",
rows: [
{ label: "Ready", value: "ready" },
{ label: "Button", value: "enabled" },
{ label: "Status", value: "ok" },
{ label: "Canonical events", value: 2 },
{ label: "Motion snapshots", value: 2 },
@@ -578,6 +648,7 @@ const renderedControlView = renderMachineSessionControlView(
lastAction: "run-gcode",
error: null,
session: expectedBundleReport.session,
runReadiness: expectedBundleReport.runReadiness,
run: expectedBundleReport.run,
},
}),
@@ -588,7 +659,7 @@ assert.equal(renderedControlView.sectionNodes.length, 3);
assert.equal(renderedControlView.sectionNodes[2].dataset.sectionId, "run");
assert.equal(
renderedControlView.sectionNodes[2].children[1].children[3].textContent,
"2",
"enabled",
);
assert.deepEqual(