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
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:
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;
|
||||
Reference in new issue
Block a user