优化前端,优化前端指令,优化自定义控制
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -35,6 +38,37 @@ def test_frontend_only_exposes_user_orchestration_and_extension() -> None:
|
||||
assert "v2RoleSelect" not in console
|
||||
|
||||
|
||||
def test_retired_layout_is_removed_instead_of_permanently_hidden() -> None:
|
||||
html = source("index.html")
|
||||
app = source("app.js")
|
||||
console = source("console-v2.js")
|
||||
styles = source("console-v2.css")
|
||||
|
||||
for retired_markup in (
|
||||
'class="topbar"',
|
||||
'class="example-group"',
|
||||
'id="safetyStrip"',
|
||||
'class="pipeline"',
|
||||
'id="gestureBackdrop"',
|
||||
):
|
||||
assert retired_markup not in html
|
||||
|
||||
for retired_runtime_hook in (
|
||||
"v2-legacy-parking",
|
||||
"v2-legacy-hidden",
|
||||
"#connectionBadge",
|
||||
"#modelBadge",
|
||||
"#hardwareBadge",
|
||||
"#executionBadge",
|
||||
"#logoutButton",
|
||||
"#gestureToggleButton",
|
||||
"#pwaBadge",
|
||||
):
|
||||
assert retired_runtime_hook not in app
|
||||
assert retired_runtime_hook not in console
|
||||
assert retired_runtime_hook not in styles
|
||||
|
||||
|
||||
def test_extension_pet_and_transient_vehicle_state_restart_contract() -> None:
|
||||
extension = source("self-extension.js")
|
||||
styles = source("self-extension.css")
|
||||
@@ -138,6 +172,278 @@ def test_vehicle_device_status_cards_prioritize_seats_and_expose_manual_controls
|
||||
assert ".v2-device-control-apply" in styles
|
||||
|
||||
|
||||
def test_passenger_can0_status_ui_uses_only_seat_ota_four_axis_contract() -> None:
|
||||
app = source("app.js")
|
||||
console = source("console-v2.js")
|
||||
|
||||
assert 'new Set(["longitudinal", "height", "backrest_recline", "legrest_height"])' in app
|
||||
assert 'if (thermalRow) thermalRow.hidden = seat === "passenger";' in app
|
||||
assert 'height: seatHeight,' in app
|
||||
assert '"seat.set_cushion_tilt": "调节座椅高度"' in app
|
||||
assert 'rows: () => [["前后", seatAxisValue("passenger", "longitudinal")], ["高度", seatAxisValue("passenger", "cushion_tilt")]' in console
|
||||
assert 'rangeControl("cushion_tilt", "高度"' in console
|
||||
assert 'rangeControl("legrest_extension"' not in console
|
||||
|
||||
|
||||
def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
|
||||
console = source("console-v2.js")
|
||||
styles = source("console-v2.css")
|
||||
|
||||
assert 'make("details", "v2-can-command-panel"' in console
|
||||
assert 'canCommandPanel.id = "v2CanCommandPanel"' in console
|
||||
assert 'id="v2CanCommandCount">0</strong>' in console
|
||||
assert 'id="v2CanCommandList"' in console
|
||||
assert "canCommandPanel.open" not in console
|
||||
assert "function parsedCanCommandForOperation(operation = {})" in console
|
||||
assert "function parsedCanCommands(result = {})" in console
|
||||
assert "getParsedCanCommands: (result) => parsedCanCommands(result)" in console
|
||||
assert "const configurations = result.function_configs || {};" in console
|
||||
assert "const plans = result.command_plans || {};" in console
|
||||
assert 'const command = (category, text, channel = "CAN1", frame = "")' in console
|
||||
assert 'data-can-channel="${escapeHtml(item.channel)}"' in console
|
||||
assert 'class="v2-can-command-channel"' in console
|
||||
assert 'window.addEventListener("cognitive-agent:result"' in console
|
||||
assert '$("#requestForm")?.addEventListener("submit"' in console
|
||||
|
||||
for command_contract in (
|
||||
"door rear ${action}",
|
||||
"door vehicle ${lockState}",
|
||||
"window ${target} ${motion}",
|
||||
"window ${target} position ${position}",
|
||||
"ac ${onOff}",
|
||||
"ac fan ${level}",
|
||||
"ac mode ${mode}",
|
||||
"ac temp ${target} ${temperature}",
|
||||
"ac control ${mode}",
|
||||
"ac cycle ${mode}",
|
||||
"outlet ${target} percent ${horizontal} ${vertical}",
|
||||
"outlet ${target} position ${horizontal} ${vertical}",
|
||||
"seat driver ${feature} ${setting}",
|
||||
"seat driver massage strength ${strength}",
|
||||
"seat driver ${axis} ${direction} ${duration}",
|
||||
"seat driver memory recall ${position}",
|
||||
"seat driver position ${position}",
|
||||
"light indoor ${onOff}",
|
||||
"light reading all ${onOff}",
|
||||
"light ambient color ${color}",
|
||||
"light ambient brightness ${level}",
|
||||
"shade position ${position}",
|
||||
"seat passenger ${axis[1]} position ${position}",
|
||||
"seat passenger dynamic start ${dynamic.name}",
|
||||
"seat passenger dynamic stop",
|
||||
"otaCan0Frame(0x64, axis[2], targetPermille)",
|
||||
"otaCan0Frame(0x66, dynamic.selector, 300)",
|
||||
"otaCan0Frame(0x63, 0xFF, 0)",
|
||||
):
|
||||
assert command_contract in console
|
||||
|
||||
assert ".v2-can-command-panel[open]" in styles
|
||||
assert ".v2-can-command-list li code" in styles
|
||||
assert ".v2-can-command-channel" in styles
|
||||
assert 'li[data-can-channel="CAN0"]' in styles
|
||||
assert ".v2-sidebar-collapsed .v2-can-command-panel" in styles
|
||||
|
||||
|
||||
def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands() -> None:
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
return
|
||||
console = source("console-v2.js")
|
||||
start = console.index(" const normalizedCanTarget")
|
||||
end = console.index("\n\n function renderParsedCanCommands", start)
|
||||
helpers = console[start:end]
|
||||
sample = {
|
||||
"function_configs": {
|
||||
"seat": {
|
||||
"spec": {
|
||||
"operations": [
|
||||
{
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target": {"zone": "front_right"},
|
||||
"parameters": {"position_pct": 36, "angle_deg": 120},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"operations": [
|
||||
{
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 24},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
"command_plans": {
|
||||
"seat": {"spec": {"commands": [{}]}},
|
||||
"native_climate": {"spec": {"commands": [{}]}},
|
||||
},
|
||||
}
|
||||
script = (
|
||||
f"{helpers}\n"
|
||||
f"const sample = {json.dumps(sample)};\n"
|
||||
"process.stdout.write(JSON.stringify(parsedCanCommands(sample)));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert json.loads(completed.stdout) == [
|
||||
{
|
||||
"channel": "CAN0",
|
||||
"category": "副驾座椅靠背调节",
|
||||
"command": "seat passenger backrest position 36",
|
||||
"frame": "0x6A0 · C7 <SEQ> 64 02 68 01 00 00",
|
||||
},
|
||||
{
|
||||
"channel": "CAN1",
|
||||
"category": "空调温度",
|
||||
"command": "ac temp driver 24",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences() -> None:
|
||||
app = source("app.js")
|
||||
styles = source("styles.css")
|
||||
html = source("index.html")
|
||||
|
||||
assert 'class="mode-editor-flow-preview empty"' in html
|
||||
assert "function modeEditorReadableCommands(flow)" in app
|
||||
assert "function modeEditorMessagesForOperation(flow, readable, operationRef, plan)" in app
|
||||
assert 'command?.signals?.special_handler === "driver_seat_memory_recall"' in app
|
||||
assert "Array.isArray(special?.frame_sequence)" in app
|
||||
assert "function modeEditorAppendCommandCard(preview, item, flow)" in app
|
||||
assert 'card.className = "mode-editor-command-card"' in app
|
||||
assert 'title.textContent = flow?.control_transport === "wireless_mqtt" ? "MQTT 消息序列"' in app
|
||||
assert 'source: "published"' in app
|
||||
assert 'source: flow?.control_transport === "wireless_mqtt" ? "projected" : "can"' in app
|
||||
assert "function_configs: functionConfigs" in app
|
||||
assert "execution_steps: executionSteps" in app
|
||||
|
||||
assert ".mode-editor-command-card" in styles
|
||||
assert ".mode-editor-command-meta" in styles
|
||||
assert ".mode-editor-message-sequence" in styles
|
||||
assert '.mode-editor-command-card[data-can-channel="CAN0"]' in styles
|
||||
|
||||
|
||||
def test_mode_editor_prioritizes_can_flow_display_area() -> None:
|
||||
app = source("app.js")
|
||||
html = source("index.html")
|
||||
styles = source("styles.css")
|
||||
|
||||
assert 'class="mode-editor-setup"' in html
|
||||
assert 'class="mode-editor-inputs"' in html
|
||||
assert "grid-template-rows: minmax(0, 30%) minmax(0, 1fr) auto auto;" in styles
|
||||
assert "grid-template-columns: minmax(150px, 0.32fr) minmax(0, 1fr);" in styles
|
||||
assert ".mode-editor-instruction-field textarea" in styles
|
||||
assert "resize: none;" in styles
|
||||
assert ".mode-editor-flow-preview" in styles
|
||||
assert "overflow: auto;" in styles
|
||||
assert 'id="modeEditorFlowExpand"' in html
|
||||
assert ".mode-editor.flow-expanded" in styles
|
||||
assert ".mode-editor-backdrop.flow-expanded" in styles
|
||||
assert "width: 100vw;" in styles
|
||||
assert "height: 100dvh;" in styles
|
||||
assert "grid-template-rows: minmax(0, 1fr);" in styles
|
||||
assert "touch-action: pan-y;" in styles
|
||||
assert "scrollbar-gutter: stable;" in styles
|
||||
assert "max-height: 220px;" not in styles
|
||||
assert "function setModeEditorFlowExpanded(expanded)" in app
|
||||
assert '$("#modeEditorFlowExpand").addEventListener("click", toggleModeEditorFlowExpanded);' in app
|
||||
|
||||
|
||||
def test_mode_editor_prepares_one_to_many_mqtt_message_sequences() -> None:
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
return
|
||||
app = source("app.js")
|
||||
console = source("console-v2.js")
|
||||
console_start = console.index(" const normalizedCanTarget")
|
||||
console_end = console.index("\n\n function renderParsedCanCommands", console_start)
|
||||
console_helpers = console[console_start:console_end]
|
||||
app_start = app.index("function modeEditorCommandCategory")
|
||||
app_end = app.index("\n\nfunction modeEditorAppendCommandCard", app_start)
|
||||
app_helpers = app[app_start:app_end]
|
||||
sample = {
|
||||
"control_transport": "wireless_mqtt",
|
||||
"function_configs": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"operations": [
|
||||
{
|
||||
"id": "temperature",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 25},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"plans": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "temperature",
|
||||
"signals": {
|
||||
"frame_id": "0x1DE",
|
||||
"data_hex": "00 00 00 00 19 00 00 00",
|
||||
"is_fd": False,
|
||||
"bitrate_switch": False,
|
||||
"debug_command": "ac temp driver 25",
|
||||
},
|
||||
"send_offset_ms": 20,
|
||||
"repeat": {"count": 3, "period_ms": 100},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"execution_steps": [],
|
||||
}
|
||||
script = (
|
||||
f"{console_helpers}\n"
|
||||
"const state = {runtime:{vehicle_mqtt:{control_topic:"
|
||||
'"n7/n7-headunit/can/command",qos:1,feedback_timeout_ms:1500}}};\n'
|
||||
"const window = {CognitiveAgentConsoleV2:{getParsedCanCommands:"
|
||||
"(result)=>parsedCanCommands(result)}};\n"
|
||||
f"{app_helpers}\n"
|
||||
f"const flow = {json.dumps(sample)};\n"
|
||||
"process.stdout.write(JSON.stringify(modeEditorReadableCommands(flow)));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
commands = json.loads(completed.stdout)
|
||||
|
||||
assert len(commands) == 1
|
||||
assert commands[0]["channel"] == "CAN1"
|
||||
assert commands[0]["category"] == "空调温度"
|
||||
assert commands[0]["command"] == "ac temp driver 25"
|
||||
assert [message["offset_ms"] for message in commands[0]["messages"]] == [
|
||||
20,
|
||||
120,
|
||||
220,
|
||||
]
|
||||
assert all(
|
||||
message["topic"] == "n7/n7-headunit/can/command"
|
||||
and message["payload"]["canId"] == "0x1DE"
|
||||
and message["payload"]["command"] == "ac temp driver 25"
|
||||
for message in commands[0]["messages"]
|
||||
)
|
||||
|
||||
|
||||
def test_interactive_console_keeps_actions_inside_composer_and_collapsed_rails_aligned() -> None:
|
||||
console = source("console-v2.js")
|
||||
styles = source("console-v2.css")
|
||||
@@ -162,15 +468,22 @@ def test_left_sync_rail_can_collapse_and_log_aligns_with_lower_workspace() -> No
|
||||
|
||||
assert 'make("section", "v2-sync-sidebar"' in console
|
||||
assert '<div class="v2-sync-sidebar-overview">' in console
|
||||
assert '$(".v2-sync-sidebar-overview", syncSidebar)?.append(sidebarFooter);' in console
|
||||
assert "syncOverview?.append(sidebarFooter);" in console
|
||||
assert '$(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);' in console
|
||||
assert "function syncSidebarLogBaseline()" in console
|
||||
assert "const requiredOverviewHeight = overviewRect" in console
|
||||
assert "overviewBottom - overviewRect.top + overviewPaddingBottom + 10" in console
|
||||
assert 'sidebar.style.setProperty("--v2-sync-log-top"' in console
|
||||
assert 'window.addEventListener("resize", syncSidebarLogBaseline);' in console
|
||||
assert "requestAnimationFrame(syncSidebarLogBaseline);" in console
|
||||
|
||||
assert "--v2-sync-log-top: 291px;" in styles
|
||||
assert "grid-template-rows: calc(var(--v2-sync-log-top) - 10px) minmax(0, 1fr);" in styles
|
||||
assert "grid-auto-rows: max-content;" in styles
|
||||
assert "align-content: start;" in styles
|
||||
assert "min-height: max-content;" in styles
|
||||
assert "align-self: start;" in styles
|
||||
assert "overflow: visible;" in styles
|
||||
assert ".v2-sync-sidebar > .v2-sync-log" in styles
|
||||
assert "grid-template-columns: 64px minmax(0, 1fr);" in styles
|
||||
assert ".v2-sidebar-collapsed #v2SidebarToggle" in styles
|
||||
|
||||
Reference in New Issue
Block a user