Files
Unlucky-Launcher/crates/app-iced/src/pages/instances/tests.rs
T

225 lines
10 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.
//! `instances` 的断言:演示数据、窄窗退化,以及**页面摆法**(源码结构断言)。
use super::*;
/// M120 的文案守门(中文无英文词 / 切语言真的变 / 页面里不许写死英文案·白名单)。
mod i18n;
use super::super::layout::source;
use super::super::layout::{frame_metrics, measure_cap_px, PaneKit, WidthClass};
/// **M102 几何判据**:列表可视区正好放下整数行——末行不会被卡片底边切半行。
///
/// 算式里的三个页面真源**直接引用**(不写字面量),所以改页面常量会让这条立刻红:
/// [`LIST_BOTTOM_INSET`](滚动区底部内缩)、[`ROW_SPACING`](页级行距,`None` = 0)。
///
/// ```text
/// 卡片高 = 760 − 标题栏 48 − 两行页头 84 − 2×gutter 24 = 580
/// 列头 = 上 12 + LabelMedium 16 + 下 4 = 32
/// 内缩 = LIST_BOTTOM_INSET = Space::X2 = 8
/// 可视区 = 580 − 32 − 8 = 540
/// 行高 = ListItem 两行档 44 + 16 = 60
/// pitch = 行高 + ROW_SPACING(None ⇒ 0) = 60
/// 整行数 = 540 / 60 = 9 行整
/// ```
///
/// **抓得住**:页面把内缩换档(`LIST_BOTTOM_INSET` → `X1` 立刻除不尽)、行距真源被改、
/// 列头算错、演示数据被缩到不够滚(独立滚动条消失);
/// **抓不住**:`list_column()` 里被另写一层 `.spacing(…)`(那一条由
/// `list_column_spaces_rows_only_from_the_shared_constant` 扫源码守)。
#[test]
fn list_viewport_fits_whole_rows_and_still_overflow() {
use unlucky_design::spacing::LayoutMetrics;
use unlucky_design::typography::TypeRole;
let lm = LayoutMetrics::DESKTOP;
let gutter = WidthClass::Expanded.gutter();
let card = 760.0 - lm.titlebar_height - lm.header_height(true) - 2.0 * gutter;
assert_eq!(card, 580.0, "1180×760 下列表卡片的内部高度");
let head = Space::X3.dp() + TypeRole::LabelMedium.style().line_height_px + Space::X1.dp();
let viewport = card - head - LIST_BOTTOM_INSET.dp();
assert_eq!(viewport, 540.0, "列表滚动区高度必须以页面的内缩真源算");
// 页级行距的**唯一真源**:None = 这一层不加间距
let gap = ROW_SPACING.map_or(0.0, Space::dp);
let row = lm.row_height + Space::X4.dp();
let pitch = row + gap;
assert_eq!(pitch, 60.0, "行节奏 = ListItem 两行档 60(不留页级行距)");
assert_eq!(
viewport % pitch,
0.0,
"可视区 {viewport} 不是 {pitch} 的整数倍 ⇒ 末行会被切半行(ROW_SPACING={ROW_SPACING:?})"
);
// 同时必须还能滚:12 行内容高过可视区,截图里才看得到独立滚动条
let content = DEMO.len() as f32 * pitch;
assert!(
content > viewport,
"列表内容 {content} 不比可视区 {viewport} 高,截图里看不到独立滚动条"
);
}
/// **源码结构**:`list_column()` 里对行容器的间距**只能**来自 [`ROW_SPACING`]。
///
/// 为什么读源码:变异实测(M102 r1)把 `.spacing(ui.space(Space::X1))` 加回行容器后,
/// 几何断言照样绿——它算的是常量,看不见页面又叠了一层。iced 的 Element 又没有内省接口,
/// 所以只能断言这段代码的形状。
///
/// ## 两层判据(r4 复审的 M4 订正)
///
/// | 层 | 判据 | 抓的是 |
/// |---|---|---|
/// | ① 构建链 | `let mut rows = column(…);` 这条语句链上不许有 `.spacing(` | 直接写在链上的间距(变异 D) |
/// | ② 语句序列 | `list_column()` 里**每一处** `rows.spacing(…)` 的实参都要**顺绑定追回 `ROW_SPACING`** | **另起一条语句**再叠间距(变异 M4,含「先把令牌挪进一条 `let`」的写法) |
///
/// 为什么②要追绑定而不是比字面:合法写法有几种形状——`rows = rows.spacing(ui.space(spacing))`
/// 直接取自 `if let Some(spacing) = ROW_SPACING`,或先 `let spacing = ROW_SPACING;` 再中转
/// (变异 H3)。`layout/source.rs::derived_from` 顺 `let` / `Some(…) =` 两种绑定往回追最多三跳,
/// 于是「真源」放行、「中转了一层 `Space::X1`」判否。
///
/// **抓得住**:构建链上的字面量间距(D)、另起语句的行距(M4)、`ROW_SPACING` / `LIST_BOTTOM_INSET`
/// 两个真源被摘掉;`column![head, scroller]` 上的合法间距不受影响(变异 J,绿);
/// **抓不住**:把间距算进别处再以非令牌形式传进 `list_row()`(那要改组件几何,代码评审能看出来)、
/// 以及绕过 `rows.spacing(…)` 这个调用形状的写法。
#[test]
fn list_column_spaces_rows_only_from_the_shared_constant() {
let file = include_str!("../instances.rs");
let body = source::function_region(file, "fn list_column");
let rows_chain = source::statement(file, "let mut rows = column(");
assert!(
body.contains("ROW_SPACING"),
"列表行距必须从 ROW_SPACING 取,不许另写:\n{body}"
);
assert!(
body.contains("LIST_BOTTOM_INSET"),
"底部内缩必须从 LIST_BOTTOM_INSET 取:\n{body}"
);
// ① 行容器的构建链上不许叠间距(`column![head, scroller]` 上的合法间距不在这条链里)
assert!(
!rows_chain.contains(".spacing("),
"行容器的构建链上又出现了间距(M102 ② 的页级附加量):\n{rows_chain}"
);
// ② 语句序列:每一处 `rows.spacing(…)` 的实参都必须**顺绑定追回 `ROW_SPACING`**
// (直接用它、或 `let spacing = ROW_SPACING` 再中转都算;先 `let extra = Space::X1;`
// 再 `rows.spacing(ui.space(extra))` 则红——r4 复审的变异 M4)
let applied = source::call_arguments(&body, "rows.spacing(");
assert!(
!applied.is_empty(),
"`list_column()` 里没有把 ROW_SPACING 应用到行容器的调用:\n{body}"
);
for argument in &applied {
let ident = argument
.split_once("ui.space(")
.map(|(_, rest)| rest)
.unwrap_or(argument);
let ident: String = ident
.chars()
.take_while(|ch| ch.is_alphanumeric() || *ch == '_')
.collect();
assert!(
argument.contains("ui.space("),
"行容器的间距实参必须走令牌栅格(`ui.space(…)`),收到 {argument:?}"
);
assert!(
!argument.contains("Space::"),
"行容器的间距实参里直接写了令牌档位:{argument:?}\n{body}"
);
assert!(
source::derived_from(&body, &ident, "ROW_SPACING"),
"行容器的间距不是从 ROW_SPACING 来的(`{ident}` 追不到 ROW_SPACING)——\
M102 ② 的页级附加量不许另起一条语句塞回来:{argument:?}\n{body}"
);
}
}
/// **散文接线**:详情页的正文段必须进 `frame::prose`(424),不许直接塞进 `frame::body`(1044)。
///
/// M102 把「块宽」与「散文列宽」拆成两个量之后,接错一行代码就会让 361 字符的正文在 1180 窗宽下
/// 排成 170 字符一行,而不红任何测试。这条断言把接线本身钉住。
#[test]
fn the_detail_note_goes_through_the_prose_box() {
let body = source::function_body(include_str!("../instances/detail.rs"), "fn push_text");
assert!(
body.contains("frame::prose("),
"正文段没有走 frame::prose:\n{body}"
);
assert!(
!body.contains("frame::body("),
"正文段被塞进了整页块(会失去 72 字符行宽承诺):\n{body}"
);
}
/// 元信息卡必须装得下**卡内最长的那条值**(视觉 r2:136 漏算 padding,版本串折行让卡底不齐)。
#[test]
fn meta_card_is_wide_enough_for_the_longest_value() {
use super::super::layout::{measure_cap_px, META_CARD_MIN_WIDTH, META_GAP};
use unlucky_design::spacing::LayoutMetrics;
// 实测(Segoe UI 14px):demo 数据里最宽的元信息值是加载器版本串
const LONGEST_VALUE: &str = "NeoForge 20.4.237";
const LONGEST_VALUE_PX: f32 = 118.0;
let padding = LayoutMetrics::DESKTOP.card_padding;
assert_eq!(padding, 20.0, "卡内边距来自令牌");
// 夹具与 demo 数据不许脱节:这个值必须真在 DEMO 里
assert!(
DEMO.iter().any(|entry| entry.loader == LONGEST_VALUE),
"夹具 {LONGEST_VALUE:?} 已经不在 DEMO 里了,请重新量最长值"
);
// ① 单卡外框减掉两侧内边距,必须放得下这条值(否则 text 会按词折行 → 卡底不齐)
let inner = META_CARD_MIN_WIDTH - 2.0 * padding;
assert!(
inner >= LONGEST_VALUE_PX,
"卡内可用 {inner}px 装不下 {LONGEST_VALUE_PX}px 的 {LONGEST_VALUE:?}(外框 {META_CARD_MIN_WIDTH} − 2×{padding})"
);
// ② 实际版面里每张卡的内宽(2 列时)也要够
let two_columns = (measure_cap_px() - META_GAP) / 2.0;
assert!(
two_columns - 2.0 * padding >= LONGEST_VALUE_PX,
"两列布局下卡内可用 {}px 装不下 {LONGEST_VALUE_PX}px",
two_columns - 2.0 * padding
);
}
#[test]
fn every_demo_entry_has_name_version_and_loader() {
for entry in &DEMO {
assert!(!entry.name.is_empty());
assert!(!entry.version.is_empty());
assert!(!entry.loader.is_empty());
}
assert!(SELECTED < DEMO.len());
}
#[test]
fn detail_paragraph_is_long_enough_to_wrap() {
// 行宽上限 ≈ 72 字符/行;写够几行才看得出断行位置
assert!(NOTE.len() > 200, "正文档太短,看不出行宽上限");
}
#[test]
fn narrow_widths_switch_the_page_to_one_column() {
// 1180 / 900 → 两列;700 → 单列(整页切换)
assert_eq!(
frame_metrics(PaneKit::ListDetail, 1180.0).layout,
PageLayout::ListDetail
);
assert_eq!(
frame_metrics(PaneKit::ListDetail, 700.0).layout,
PageLayout::SingleColumn
);
// 窄窗下详情不吃满窗口:正文列仍 ≤ 行宽上限
let narrow = frame_metrics(PaneKit::ListDetail, 700.0);
assert!(narrow.column <= measure_cap_px() + f32::EPSILON);
assert!(narrow.secondary.is_none());
}