mirror of
https://github.com/bybrooklyn/openbitdo.git
synced 2026-03-19 12:12:57 -04:00
release prep: rc.1 baseline and gating updates
This commit is contained in:
19
sdk/crates/bitdo_tui/Cargo.toml
Normal file
19
sdk/crates/bitdo_tui/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "bitdo_tui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
ratatui = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
bitdo_proto = { path = "../bitdo_proto" }
|
||||
bitdo_app_core = { path = "../bitdo_app_core" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
69
sdk/crates/bitdo_tui/src/desktop_io.rs
Normal file
69
sdk/crates/bitdo_tui/src/desktop_io.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// Open a file or directory with the user's default desktop application.
|
||||
pub(crate) fn open_path_with_default_app(path: &Path) -> Result<()> {
|
||||
let mut cmd = if cfg!(target_os = "macos") {
|
||||
let mut c = Command::new("open");
|
||||
c.arg(path);
|
||||
c
|
||||
} else {
|
||||
let mut c = Command::new("xdg-open");
|
||||
c.arg(path);
|
||||
c
|
||||
};
|
||||
let status = cmd.status()?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"failed to open path with default app: {}",
|
||||
path.to_string_lossy()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy text into the system clipboard using platform-appropriate commands.
|
||||
pub(crate) fn copy_text_to_clipboard(text: &str) -> Result<()> {
|
||||
if cfg!(target_os = "macos") {
|
||||
return copy_via_command("pbcopy", &[], text);
|
||||
}
|
||||
|
||||
if command_exists("wl-copy") {
|
||||
return copy_via_command("wl-copy", &[], text);
|
||||
}
|
||||
if command_exists("xclip") {
|
||||
return copy_via_command("xclip", &["-selection", "clipboard"], text);
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"no clipboard utility found (tried pbcopy/wl-copy/xclip)"
|
||||
))
|
||||
}
|
||||
|
||||
fn command_exists(name: &str) -> bool {
|
||||
Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("command -v {name} >/dev/null 2>&1"))
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn copy_via_command(command: &str, args: &[&str], text: &str) -> Result<()> {
|
||||
let mut child = Command::new(command)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()?;
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
use std::io::Write as _;
|
||||
stdin.write_all(text.as_bytes())?;
|
||||
}
|
||||
let status = child.wait()?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("clipboard command failed: {command}"))
|
||||
}
|
||||
}
|
||||
3151
sdk/crates/bitdo_tui/src/lib.rs
Normal file
3151
sdk/crates/bitdo_tui/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
38
sdk/crates/bitdo_tui/src/settings.rs
Normal file
38
sdk/crates/bitdo_tui/src/settings.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::ReportSaveMode;
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct PersistedSettings {
|
||||
#[serde(default = "default_settings_schema_version")]
|
||||
schema_version: u32,
|
||||
#[serde(default)]
|
||||
advanced_mode: bool,
|
||||
#[serde(default)]
|
||||
report_save_mode: ReportSaveMode,
|
||||
}
|
||||
|
||||
const fn default_settings_schema_version() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// Persist beginner/advanced preferences in a small TOML config file.
|
||||
pub(crate) fn persist_user_settings(
|
||||
path: &Path,
|
||||
advanced_mode: bool,
|
||||
report_save_mode: ReportSaveMode,
|
||||
) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let body = toml::to_string_pretty(&PersistedSettings {
|
||||
schema_version: default_settings_schema_version(),
|
||||
advanced_mode,
|
||||
report_save_mode,
|
||||
})
|
||||
.map_err(|err| anyhow!("failed to serialize user settings: {err}"))?;
|
||||
std::fs::write(path, body)?;
|
||||
Ok(())
|
||||
}
|
||||
217
sdk/crates/bitdo_tui/src/support_report.rs
Normal file
217
sdk/crates/bitdo_tui/src/support_report.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use crate::AppDevice;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bitdo_app_core::FirmwareFinalReport;
|
||||
use bitdo_proto::{DiagProbeResult, SupportLevel, SupportTier};
|
||||
use chrono::Utc;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
const REPORT_MAX_COUNT: usize = 20;
|
||||
const REPORT_MAX_AGE_DAYS: u64 = 30;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct SupportReport {
|
||||
schema_version: u32,
|
||||
generated_at_utc: String,
|
||||
operation: String,
|
||||
device: Option<SupportReportDevice>,
|
||||
status: String,
|
||||
message: String,
|
||||
diag: Option<DiagProbeResult>,
|
||||
firmware: Option<FirmwareFinalReport>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct SupportReportDevice {
|
||||
vid: u16,
|
||||
pid: u16,
|
||||
name: String,
|
||||
canonical_id: String,
|
||||
runtime_label: String,
|
||||
serial: Option<String>,
|
||||
support_level: String,
|
||||
support_tier: String,
|
||||
}
|
||||
|
||||
/// Persist a troubleshooting report as TOML.
|
||||
///
|
||||
/// Reports are intended for failure/support paths and are named with a timestamp plus
|
||||
/// a serial-or-VID/PID token so users can share deterministic artifacts with support.
|
||||
pub(crate) async fn persist_support_report(
|
||||
operation: &str,
|
||||
device: Option<&AppDevice>,
|
||||
status: &str,
|
||||
message: String,
|
||||
diag: Option<&DiagProbeResult>,
|
||||
firmware: Option<&FirmwareFinalReport>,
|
||||
) -> Result<PathBuf> {
|
||||
let now = Utc::now();
|
||||
let report = SupportReport {
|
||||
schema_version: 1,
|
||||
generated_at_utc: now.to_rfc3339(),
|
||||
operation: operation.to_owned(),
|
||||
device: device.map(|d| SupportReportDevice {
|
||||
vid: d.vid_pid.vid,
|
||||
pid: d.vid_pid.pid,
|
||||
name: d.name.clone(),
|
||||
canonical_id: d.name.clone(),
|
||||
runtime_label: d.support_status().as_str().to_owned(),
|
||||
serial: d.serial.clone(),
|
||||
support_level: match d.support_level {
|
||||
SupportLevel::Full => "full".to_owned(),
|
||||
SupportLevel::DetectOnly => "detect-only".to_owned(),
|
||||
},
|
||||
support_tier: match d.support_tier {
|
||||
SupportTier::Full => "full".to_owned(),
|
||||
SupportTier::CandidateReadOnly => "candidate-readonly".to_owned(),
|
||||
SupportTier::DetectOnly => "detect-only".to_owned(),
|
||||
},
|
||||
}),
|
||||
status: status.to_owned(),
|
||||
message,
|
||||
diag: diag.cloned(),
|
||||
firmware: firmware.cloned(),
|
||||
};
|
||||
|
||||
let report_dir = default_report_directory();
|
||||
tokio::fs::create_dir_all(&report_dir).await?;
|
||||
|
||||
let token = report_subject_token(device);
|
||||
let file_name = format!(
|
||||
"{}-{}-{}.toml",
|
||||
sanitize_token(operation),
|
||||
now.format("%Y%m%d-%H%M%S"),
|
||||
token
|
||||
);
|
||||
let path = report_dir.join(file_name);
|
||||
|
||||
let body = toml::to_string_pretty(&report)
|
||||
.map_err(|err| anyhow!("failed to serialize support report: {err}"))?;
|
||||
tokio::fs::write(&path, body).await?;
|
||||
let _ = prune_reports_on_write().await;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Startup pruning is age-based to keep stale files out of user systems.
|
||||
pub(crate) async fn prune_reports_on_startup() -> Result<()> {
|
||||
prune_reports_by_age().await
|
||||
}
|
||||
|
||||
/// Write-time pruning is count-based to keep growth bounded deterministically.
|
||||
async fn prune_reports_on_write() -> Result<()> {
|
||||
prune_reports_by_count().await
|
||||
}
|
||||
|
||||
pub(crate) fn report_subject_token(device: Option<&AppDevice>) -> String {
|
||||
if let Some(device) = device {
|
||||
if let Some(serial) = device.serial.as_deref() {
|
||||
let cleaned = sanitize_token(serial);
|
||||
if !cleaned.is_empty() {
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
return format!("{:04x}{:04x}", device.vid_pid.vid, device.vid_pid.pid);
|
||||
}
|
||||
|
||||
"unknown".to_owned()
|
||||
}
|
||||
|
||||
fn sanitize_token(value: &str) -> String {
|
||||
let mut out = String::with_capacity(value.len());
|
||||
for ch in value.chars() {
|
||||
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
|
||||
out.push(ch);
|
||||
} else {
|
||||
out.push('_');
|
||||
}
|
||||
}
|
||||
|
||||
out.trim_matches('_').to_owned()
|
||||
}
|
||||
|
||||
fn default_report_directory() -> PathBuf {
|
||||
if cfg!(target_os = "macos") {
|
||||
return home_directory()
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("OpenBitdo")
|
||||
.join("reports");
|
||||
}
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
if let Some(xdg_data_home) = std::env::var_os("XDG_DATA_HOME") {
|
||||
return PathBuf::from(xdg_data_home)
|
||||
.join("openbitdo")
|
||||
.join("reports");
|
||||
}
|
||||
|
||||
return home_directory()
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("openbitdo")
|
||||
.join("reports");
|
||||
}
|
||||
|
||||
std::env::temp_dir().join("openbitdo").join("reports")
|
||||
}
|
||||
|
||||
async fn list_report_files() -> Result<Vec<PathBuf>> {
|
||||
let report_dir = default_report_directory();
|
||||
let mut out = Vec::new();
|
||||
let mut entries = match tokio::fs::read_dir(&report_dir).await {
|
||||
Ok(entries) => entries,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(out),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("toml") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
async fn prune_reports_by_count() -> Result<()> {
|
||||
let mut files = list_report_files().await?;
|
||||
files.sort_by_key(|path| {
|
||||
std::fs::metadata(path)
|
||||
.and_then(|meta| meta.modified())
|
||||
.unwrap_or(SystemTime::UNIX_EPOCH)
|
||||
});
|
||||
files.reverse();
|
||||
|
||||
for path in files.into_iter().skip(REPORT_MAX_COUNT) {
|
||||
let _ = tokio::fs::remove_file(path).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prune_reports_by_age() -> Result<()> {
|
||||
let now = SystemTime::now();
|
||||
let max_age = Duration::from_secs(REPORT_MAX_AGE_DAYS * 24 * 60 * 60);
|
||||
for path in list_report_files().await? {
|
||||
let Ok(meta) = std::fs::metadata(&path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(modified) = meta.modified() else {
|
||||
continue;
|
||||
};
|
||||
if now.duration_since(modified).unwrap_or_default() > max_age {
|
||||
let _ = tokio::fs::remove_file(path).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn home_directory() -> PathBuf {
|
||||
std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
}
|
||||
370
sdk/crates/bitdo_tui/src/tests.rs
Normal file
370
sdk/crates/bitdo_tui/src/tests.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
use super::*;
|
||||
use crate::support_report::report_subject_token;
|
||||
use bitdo_app_core::{FirmwareOutcome, OpenBitdoCoreConfig};
|
||||
use bitdo_proto::SupportLevel;
|
||||
|
||||
#[test]
|
||||
fn about_state_roundtrip_returns_home() {
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(vec![AppDevice {
|
||||
vid_pid: VidPid::new(0x2dc8, 0x6009),
|
||||
name: "Test".to_owned(),
|
||||
support_level: SupportLevel::Full,
|
||||
support_tier: SupportTier::Full,
|
||||
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
|
||||
capability: bitdo_proto::PidCapability::full(),
|
||||
evidence: bitdo_proto::SupportEvidence::Confirmed,
|
||||
serial: Some("SERIAL1".to_owned()),
|
||||
connected: true,
|
||||
}]);
|
||||
app.open_about();
|
||||
assert_eq!(app.state, TuiWorkflowState::About);
|
||||
app.close_overlay();
|
||||
assert_eq!(app.state, TuiWorkflowState::Home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_devices_without_any_device_enters_wait_state() {
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(Vec::new());
|
||||
assert_eq!(app.state, TuiWorkflowState::WaitForDevice);
|
||||
assert!(app.selected.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_devices_autoselects_single_device() {
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(vec![AppDevice {
|
||||
vid_pid: VidPid::new(0x2dc8, 0x6009),
|
||||
name: "One".to_owned(),
|
||||
support_level: SupportLevel::Full,
|
||||
support_tier: SupportTier::Full,
|
||||
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
|
||||
capability: bitdo_proto::PidCapability::full(),
|
||||
evidence: bitdo_proto::SupportEvidence::Confirmed,
|
||||
serial: None,
|
||||
connected: true,
|
||||
}]);
|
||||
|
||||
assert_eq!(app.state, TuiWorkflowState::Home);
|
||||
assert_eq!(app.selected_index, 0);
|
||||
assert_eq!(app.selected, Some(VidPid::new(0x2dc8, 0x6009)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serial_token_prefers_serial_then_vidpid() {
|
||||
let with_serial = AppDevice {
|
||||
vid_pid: VidPid::new(0x2dc8, 0x6009),
|
||||
name: "S".to_owned(),
|
||||
support_level: SupportLevel::Full,
|
||||
support_tier: SupportTier::Full,
|
||||
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
|
||||
capability: bitdo_proto::PidCapability::full(),
|
||||
evidence: bitdo_proto::SupportEvidence::Confirmed,
|
||||
serial: Some("ABC 123".to_owned()),
|
||||
connected: true,
|
||||
};
|
||||
assert_eq!(report_subject_token(Some(&with_serial)), "ABC_123");
|
||||
|
||||
let without_serial = AppDevice {
|
||||
serial: None,
|
||||
..with_serial
|
||||
};
|
||||
assert_eq!(report_subject_token(Some(&without_serial)), "2dc86009");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_options_default_to_failure_only_reports() {
|
||||
let opts = TuiLaunchOptions::default();
|
||||
assert_eq!(opts.report_save_mode, ReportSaveMode::FailureOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_panel_text_matches_support_tier() {
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(vec![AppDevice {
|
||||
vid_pid: VidPid::new(0x2dc8, 0x2100),
|
||||
name: "Candidate".to_owned(),
|
||||
support_level: SupportLevel::DetectOnly,
|
||||
support_tier: SupportTier::CandidateReadOnly,
|
||||
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
|
||||
capability: bitdo_proto::PidCapability {
|
||||
supports_mode: true,
|
||||
supports_profile_rw: true,
|
||||
supports_boot: false,
|
||||
supports_firmware: false,
|
||||
supports_jp108_dedicated_map: false,
|
||||
supports_u2_slot_config: false,
|
||||
supports_u2_button_map: false,
|
||||
},
|
||||
evidence: bitdo_proto::SupportEvidence::Inferred,
|
||||
serial: None,
|
||||
connected: true,
|
||||
}]);
|
||||
let selected = app.selected_device().expect("selected");
|
||||
let text = blocked_action_panel_text(selected);
|
||||
assert!(text.contains("blocked"));
|
||||
assert!(text.contains("Status shown as Blocked"));
|
||||
assert_eq!(beginner_status_label(selected), "Blocked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_advanced_report_mode_skips_off_setting() {
|
||||
let mut app = TuiApp {
|
||||
advanced_mode: false,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(app.report_save_mode, ReportSaveMode::FailureOnly);
|
||||
app.cycle_report_save_mode().expect("cycle");
|
||||
assert_eq!(app.report_save_mode, ReportSaveMode::Always);
|
||||
app.cycle_report_save_mode().expect("cycle");
|
||||
assert_eq!(app.report_save_mode, ReportSaveMode::FailureOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_device_label_is_beginner_friendly() {
|
||||
let device = AppDevice {
|
||||
vid_pid: VidPid::new(0x2dc8, 0xabcd),
|
||||
name: "PID_UNKNOWN".to_owned(),
|
||||
support_level: SupportLevel::DetectOnly,
|
||||
support_tier: SupportTier::DetectOnly,
|
||||
protocol_family: bitdo_proto::ProtocolFamily::Unknown,
|
||||
capability: bitdo_proto::PidCapability::identify_only(),
|
||||
evidence: bitdo_proto::SupportEvidence::Untested,
|
||||
serial: None,
|
||||
connected: true,
|
||||
};
|
||||
let label = super::display_device_name(&device);
|
||||
assert!(label.contains("Unknown 8BitDo Device"));
|
||||
assert!(label.contains("2dc8:abcd"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn home_refresh_loads_devices() {
|
||||
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
|
||||
mock_mode: true,
|
||||
default_chunk_size: 16,
|
||||
progress_interval_ms: 1,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(core.list_devices().await.expect("devices"));
|
||||
|
||||
assert!(!app.devices.is_empty());
|
||||
assert!(app.selected_device().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_tui_app_no_ui_blocks_detect_only_pid() {
|
||||
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
|
||||
mock_mode: true,
|
||||
default_chunk_size: 16,
|
||||
progress_interval_ms: 1,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = run_tui_app(
|
||||
core,
|
||||
TuiLaunchOptions {
|
||||
no_ui: true,
|
||||
selected_vid_pid: Some(VidPid::new(0x2dc8, 0x2100)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_tui_app_no_ui_full_support_completes() {
|
||||
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
|
||||
mock_mode: true,
|
||||
default_chunk_size: 16,
|
||||
progress_interval_ms: 1,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
run_tui_app(
|
||||
core,
|
||||
TuiLaunchOptions {
|
||||
no_ui: true,
|
||||
selected_vid_pid: Some(VidPid::new(0x2dc8, 0x6009)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("run app");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tui_flow_with_manual_path_completes() {
|
||||
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
|
||||
mock_mode: true,
|
||||
default_chunk_size: 16,
|
||||
progress_interval_ms: 1,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let path = std::env::temp_dir().join("openbitdo-tui-flow.bin");
|
||||
tokio::fs::write(&path, vec![1u8; 128])
|
||||
.await
|
||||
.expect("write");
|
||||
|
||||
let report = run_tui_flow(
|
||||
core,
|
||||
TuiRunRequest {
|
||||
vid_pid: VidPid::new(0x2dc8, 0x6009),
|
||||
firmware_path: path.clone(),
|
||||
allow_unsafe: true,
|
||||
brick_risk_ack: true,
|
||||
experimental: true,
|
||||
chunk_size: Some(32),
|
||||
acknowledged_risk: true,
|
||||
no_ui: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("run tui flow");
|
||||
|
||||
assert_eq!(report.status, FirmwareOutcome::Completed);
|
||||
let _ = tokio::fs::remove_file(path).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn support_report_is_toml_file() {
|
||||
let device = AppDevice {
|
||||
vid_pid: VidPid::new(0x2dc8, 0x6009),
|
||||
name: "Test".to_owned(),
|
||||
support_level: SupportLevel::Full,
|
||||
support_tier: SupportTier::Full,
|
||||
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
|
||||
capability: bitdo_proto::PidCapability::full(),
|
||||
evidence: bitdo_proto::SupportEvidence::Confirmed,
|
||||
serial: Some("RPT-1".to_owned()),
|
||||
connected: true,
|
||||
};
|
||||
|
||||
let report_path = persist_support_report(
|
||||
"diag-probe",
|
||||
Some(&device),
|
||||
"ok",
|
||||
"all checks passed".to_owned(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("report path");
|
||||
|
||||
assert_eq!(
|
||||
report_path.extension().and_then(|s| s.to_str()),
|
||||
Some("toml")
|
||||
);
|
||||
let _ = tokio::fs::remove_file(report_path).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_action_enters_jp108_wizard_for_jp108_device() {
|
||||
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
|
||||
mock_mode: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(core.list_devices().await.expect("devices"));
|
||||
let jp108_idx = app
|
||||
.devices
|
||||
.iter()
|
||||
.position(|d| d.vid_pid.pid == 0x5209)
|
||||
.expect("jp108 fixture");
|
||||
app.select_index(jp108_idx);
|
||||
app.state = TuiWorkflowState::Home;
|
||||
|
||||
let mut terminal = None;
|
||||
let mut events = None;
|
||||
let opts = TuiLaunchOptions::default();
|
||||
execute_home_action(
|
||||
&core,
|
||||
&mut terminal,
|
||||
&mut app,
|
||||
&opts,
|
||||
&mut events,
|
||||
HomeAction::Update,
|
||||
)
|
||||
.await
|
||||
.expect("execute");
|
||||
|
||||
assert_eq!(app.state, TuiWorkflowState::Jp108Mapping);
|
||||
assert!(!app.jp108_mappings.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_action_enters_u2_wizard_for_ultimate2_device() {
|
||||
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
|
||||
mock_mode: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(core.list_devices().await.expect("devices"));
|
||||
let u2_idx = app
|
||||
.devices
|
||||
.iter()
|
||||
.position(|d| d.vid_pid.pid == 0x6012)
|
||||
.expect("u2 fixture");
|
||||
app.select_index(u2_idx);
|
||||
app.state = TuiWorkflowState::Home;
|
||||
|
||||
let mut terminal = None;
|
||||
let mut events = None;
|
||||
let opts = TuiLaunchOptions::default();
|
||||
execute_home_action(
|
||||
&core,
|
||||
&mut terminal,
|
||||
&mut app,
|
||||
&opts,
|
||||
&mut events,
|
||||
HomeAction::Update,
|
||||
)
|
||||
.await
|
||||
.expect("execute");
|
||||
|
||||
assert_eq!(app.state, TuiWorkflowState::U2CoreProfile);
|
||||
assert!(app.u2_profile.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn device_flow_backup_apply_sets_backup_id() {
|
||||
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
|
||||
mock_mode: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut app = TuiApp::default();
|
||||
app.refresh_devices(core.list_devices().await.expect("devices"));
|
||||
let jp108_idx = app
|
||||
.devices
|
||||
.iter()
|
||||
.position(|d| d.vid_pid.pid == 0x5209)
|
||||
.expect("jp108 fixture");
|
||||
app.select_index(jp108_idx);
|
||||
app.begin_jp108_mapping(
|
||||
core.jp108_read_dedicated_mapping(VidPid::new(0x2dc8, 0x5209))
|
||||
.await
|
||||
.expect("read"),
|
||||
);
|
||||
|
||||
let mut terminal = None;
|
||||
let mut events = None;
|
||||
let opts = TuiLaunchOptions::default();
|
||||
execute_device_flow_action(
|
||||
&core,
|
||||
&mut terminal,
|
||||
&mut app,
|
||||
&opts,
|
||||
&mut events,
|
||||
DeviceFlowAction::BackupApply,
|
||||
)
|
||||
.await
|
||||
.expect("apply");
|
||||
|
||||
assert!(app.latest_backup.is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user