release: v0.2.5
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

This commit is contained in:
AskaEth committed 2026-11-12 12:06:59 +08:00
commit 0522a41a52
1587 files changed
+323674

No files matched your search

@@ -0,0 +1,257 @@
//! 「存档区」(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;