Files
Unlucky-Launcher/crates/app-iced/src/pages/instances/tests/saves.rs
T
AskaEth 0522a41a52
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
release: v0.2.5
2026-11-12 12:06:59 +08:00

258 lines
9.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 「存档区」(M235)的**模型**判据:快照 → 页面要画的东西。
//!
//! 为什么只测一个纯函数:iced 0.14 的 `Element` 没有内省接口(与 `tests/launch_args.rs`
//! 同一取舍),画出来的行在测试里拿不到。所以把"快照 → 行/计数/失败线"全部压在
//! [`saves::from_state`] 里,在这里逐条钉住——页面那一支只负责把这些字段画出来。
//!
//! 守门形状:`detail.rs` 必须调 `saves::section(ui, saves, …)`(`instances/tests.rs` 的源码
//! 结构断言盯着 `detail_column` 里 MODS 与 SAVES 的先后;这里守模型)。
use unlucky_presenter::dto::{
SavesArea, SavesFailureKind, SavesFailureView, SavesListView, SavesSkipKind, SavesSkipView,
ScreenshotPreview, ScreenshotView, ThumbnailView, WorldView,
};
use unlucky_presenter::state::SavesState;
use super::super::saves::{failure_line, from_state};
use super::super::Texts;
use unlucky_i18n::Lang;
/// 2024-05-01 10:00 UTC(UTC 毫秒)。
const STAMP: u64 = 1_714_557_600_000;
fn view(instance: &str) -> SavesListView {
SavesListView {
instance: instance.to_string(),
dir: "C:/mc/saves".to_string(),
exists: true,
worlds: vec![WorldView {
dir: "Better World".to_string(),
icon: true,
modified_ms: Some(STAMP),
}],
screenshots_dir: "C:/mc/screenshots".to_string(),
screenshots_exists: true,
screenshots: vec![ScreenshotView {
file: "2024-05-01_10.00.00.png".to_string(),
bytes: 2_048,
modified_ms: None,
// M251:一像素的缩略图——这条用例只关心「格子/计数/副文案」的映射,不关心解码。
preview: ScreenshotPreview::Ready(ThumbnailView {
width: 1,
height: 1,
pixels: vec![0x2E, 0x7D, 0x4F, 0xFF],
}),
}],
skipped: vec![SavesSkipView {
area: SavesArea::Screenshots,
file: "notes.txt".to_string(),
reason: SavesSkipKind::NotAnImage,
}],
last_op: None,
}
}
/// 已绑定实例的视图态(`bind` 是 presenter 内部的 `pub(super)`,这里直接写字段——
/// 它本来就是公开状态的一部分,与 `AppState::bind_saves` 走的是同一条路)。
fn bound_state(instance: &str) -> SavesState {
SavesState {
bound: Some(instance.to_string()),
..SavesState::default()
}
}
/// 快照 → 行:世界带「有图标」,截图带大小;时间**按 UTC 切**,取不到就说"时间未知"。
#[test]
fn the_snapshot_becomes_rows_with_stamps_and_badges() {
let mut state = bound_state("a");
state.changed(view("a"));
let saves = from_state(&state, Lang::ZhCn.catalog());
assert!(!saves.loading);
assert!(!saves.retryable, "成功的快照不该带重试");
assert_eq!(saves.worlds.len(), 1);
assert_eq!(saves.worlds[0].name, "Better World");
let zh_icon = Texts::of(Lang::ZhCn.catalog()).saves_world_icon();
assert!(
saves.worlds[0].detail.contains("2024-05-01 10:00 UTC"),
"世界的副文案要带 UTC 时间:{:?}",
saves.worlds[0].detail
);
assert!(
saves.worlds[0].detail.contains(zh_icon),
"有 icon.png 的世界要带标记:{:?}",
saves.worlds[0].detail
);
assert_eq!(saves.screenshots.len(), 1);
assert_eq!(
saves.thumbnails(),
1,
"带了像素的截图必须变成一格真图(不是占位)"
);
assert!(
saves.screenshots[0].detail.contains("2.0 KB"),
"截图的副文案要带大小:{:?}",
saves.screenshots[0].detail
);
assert!(
saves.screenshots[0]
.detail
.contains(Texts::of(Lang::ZhCn.catalog()).saves_time_unknown()),
"mtime 缺失时要说『时间未知』,不许拿 0 或当前时间糊:{:?}",
saves.screenshots[0].detail
);
// 计数与跳过:3 个世界 / 12 张截图这种句子在这一层拼好。
assert_eq!(saves.count, "1 个世界 · 1 张截图");
assert_eq!(saves.skipped, 1);
assert_eq!(saves.skipped_note, "跳过 1 项");
}
/// 首帧(已绑定、还没有快照)是 `loading`;没有绑定则什么都不算"已加载"。
#[test]
fn a_bound_instance_without_a_snapshot_is_loading() {
let state = bound_state("a");
let saves = from_state(&state, Lang::En.catalog());
assert!(saves.loading);
assert!(!saves.loaded());
let idle = from_state(&SavesState::default(), Lang::En.catalog());
assert!(!idle.loading, "不在详情页不该画『正在读取』");
assert!(!idle.loaded());
}
/// 两个目录都不存在 ⇒ `missing`(画"还没有目录"那句,而不是"这里空空的")。
/// 目录在但里面什么都没有 ⇒ `nothing_yet`。
#[test]
fn missing_directories_and_empty_ones_are_different_states() {
let mut state = bound_state("a");
let mut gone = view("a");
gone.exists = false;
gone.screenshots_exists = false;
gone.worlds.clear();
gone.screenshots.clear();
gone.skipped.clear();
state.changed(gone);
let saves = from_state(&state, Lang::En.catalog());
assert!(saves.missing);
assert!(!saves.nothing_yet(), "缺目录不该被当成『目录在但是空的』");
let mut state = bound_state("a");
let mut empty = view("a");
empty.worlds.clear();
empty.screenshots.clear();
empty.skipped.clear();
state.changed(empty);
let saves = from_state(&state, Lang::En.catalog());
assert!(!saves.missing);
assert!(saves.nothing_yet());
}
/// 失败:读盘失败**可重试**,打开文件夹失败**不可重试**(重试按钮会去重新列目录,那是错的动作)。
///
/// 抓得住:把两种失败合成一条「重试」;也抓得住"失败顺手把列表清掉"(用户看不见上一次读到的东西)。
#[test]
fn only_list_failures_offer_a_retry() {
for (kind, retryable) in [
(SavesFailureKind::DirUnusable, true),
(SavesFailureKind::InstanceUnavailable, true),
(SavesFailureKind::OpenFailed, false),
] {
let mut state = bound_state("a");
state.changed(view("a"));
state.failed(SavesFailureView {
instance: "a".to_string(),
file: String::new(),
op: None,
kind,
detail: "Access is denied.".to_string(),
});
let saves = from_state(&state, Lang::En.catalog());
assert!(!saves.loading);
assert_eq!(saves.retryable, retryable, "{kind:?} 的重试口径错了");
assert!(saves.error.is_some(), "{kind:?} 没有画出失败");
assert_eq!(saves.worlds.len(), 1, "{kind:?} 把旧列表清掉了");
assert_eq!(saves.screenshots.len(), 1);
}
}
/// **只拿到失败、一份快照都没有**时,不许被画成"这里还没有世界"。
///
/// 抓得住:`nothing_yet()` 只看"两个列表都空"——那样"读不了"会顶着两句"这里空空的",
/// 用户会把权限问题读成"我没有存档"。
#[test]
fn a_failure_without_a_snapshot_is_not_an_empty_list() {
let mut state = bound_state("a");
state.failed(SavesFailureView {
instance: "a".to_string(),
file: String::new(),
op: None,
kind: SavesFailureKind::DirUnusable,
detail: "Access is denied.".to_string(),
});
let saves = from_state(&state, Lang::En.catalog());
assert!(saves.error.is_some());
assert!(!saves.has_view, "没有快照却记成了有");
assert!(!saves.nothing_yet(), "纯失败态被画成了空列表");
assert_eq!(saves.count, "", "没有快照不该画计数");
}
/// 失败主句跟着语言与大类走,OS 原文只进括号(M161 的文案纪律)。
#[test]
fn the_failure_line_follows_the_language_and_keeps_the_detail_in_brackets() {
let failure = SavesFailureView {
instance: "a".to_string(),
file: String::new(),
op: None,
kind: SavesFailureKind::DirUnusable,
detail: "Access is denied.".to_string(),
};
let zh = failure_line(&failure, Texts::of(Lang::ZhCn.catalog()));
assert!(zh.starts_with("读取 saves/ 失败:"), "{zh}");
assert!(zh.ends_with("(Access is denied.)"), "{zh}");
let en = failure_line(&failure, Texts::of(Lang::En.catalog()));
assert!(en.starts_with("Could not read saves/:"), "{en}");
assert!(en.contains("Access is denied."), "{en}");
// 没有原文时不留一对空括号。
let bare = SavesFailureView {
detail: String::new(),
..failure
};
let text = failure_line(&bare, Texts::of(Lang::ZhCn.catalog()));
assert!(!text.contains('('), "空原文留下了一对括号:{text}");
}
/// 时间换算:毫秒 → `YYYY-MM-DD HH:MM UTC`(跨年/闰日各钉一条,别只信一条)。
#[test]
fn millisecond_stamps_are_rendered_in_utc() {
let stamp = |ms: u64| {
let mut state = bound_state("a");
let mut view = view("a");
view.worlds[0].modified_ms = Some(ms);
state.changed(view);
from_state(&state, Lang::En.catalog()).worlds[0]
.detail
.clone()
};
// 1970-01-01 00:00 UTC(纪元)
assert!(stamp(0).starts_with("1970-01-01 00:00 UTC"), "{}", stamp(0));
// 2024-05-01 10:00 UTC
assert!(
stamp(STAMP).starts_with("2024-05-01 10:00 UTC"),
"{}",
stamp(STAMP)
);
// 2024-02-29 23:59 UTC(闰日)
assert!(
stamp(1_709_251_140_000).starts_with("2024-02-29 23:59 UTC"),
"{}",
stamp(1_709_251_140_000)
);
}
mod preview;
mod write;