Files
Unlucky-Launcher/crates/app-iced/src/components/selection/probe.rs
T
AskaEth 9519ce8e1b test(selection): 状态层的两种 α 都要查(复审 P2-4 补,第二次变异曾漏网)
原来只查 hover(8%)那一色,于是「把 pressed 列的状态层画成方块」照样绿——实测变异
mut-one-layer-box 当时是 passed。现在 hover(8%) 与 pressed/focus(10%) 两种颜色各查一遍,
每一块的四个角都不许有墨。

变异证据(六个变异全部转红,见报告):
- mut-one-box-22     → 18.3:Primary 块不得超过 21,实测 22×22 @ (541,222)
- mut-one-layer-box  → 18.5:Pressed 状态层必须是圆(四角不该有墨)@ (532,212)
- mut-chip-34        → 13.1:filter chip 应恰好四块,实测 3
2026-11-02 07:03:13 +08:00

327 lines
13 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.
//! 选择类家族的**渲染层**几何断言:对着真截图量像素(M68 的教训:纯函数对、图可能是错的)。
//!
//! 为什么住在 `selection/` 而不是 `gallery/`:`gallery/selection.rs` 只负责「把矩阵摆进 harness」,
//! 断言加上去会把它顶到 400 行红线。像素扫描的 4 个纯函数在 [`super::pixel`](同一目录、`cfg(test)`)。
//!
//! 量什么(每条对一个具体错误,容差 ±1):
//!
//! - **Checkbox 外缘 20,且选中/未选中同尺寸**(§18.3:描边向内收;向外画会变 22)——逐块断言;
//! - **Radio 内点 10 不是 12**(§19.2)· 环是真圆、不是圆角方(§19.6);
//! - **状态层直径 40 且四角无墨**(§18.5 / §19.7)——**每一块**都查;
//! - 勾形墨迹 ≈14.8(端点 12 + 方头外扩)· **indeterminate 是短横**(§18.4/§18.6);
//! - Switch 轨道**恰好一块** 52×32(§10.1)· Chip **恰好一块** 32 高(§13.1);
//! - 禁用:chip 只降不删(P2-1)· 选中禁用的 Checkbox 容器是 38% 不是 12%(P2-3)。
//!
//! 复现(worktree 根目录):
//!
//! ```text
//! cargo build -p unlucky-app-iced --bin unlucky-launcher-iced
//! ULL_GALLERY_SECTION=selection ./target/debug/unlucky-launcher-iced \
//! --software-renderer --screen=gallery \
//! --screenshot target/m76/selection-light.png --screenshot-theme=light
//! ULL_GALLERY_SHOT=target/m76/selection-light.png cargo test -p unlucky-app-iced selection::probe
//! ```
use unlucky_design::color::{Role, SchemeFlavor};
use unlucky_design::typography::Platform;
use super::super::interaction::{self, Interaction};
use super::pixel::{center, dims, ink_boxes, inner_ink, matches, near, rgb, shot_path, Shot};
use crate::theme::{Families, Ui};
const FAMILIES: Families = Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
};
fn ui(flavor: SchemeFlavor) -> Ui {
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
}
/// 一条测试覆盖全部几何断言(浅/深各跑一次)。
#[test]
fn selection_geometry_matches_the_reference() {
let Some(path) = shot_path() else {
eprintln!(
"[selection-probe] SKIPPED:未设置 ULL_GALLERY_SHOT。这是**渲染层**几何断言,\
需要先出图(见本文件模块文档,注意 ULL_GALLERY_SECTION=selection)。"
);
return;
};
let shot = Shot::load(&path).expect("读 selection 截图");
assert_eq!(
(shot.width, shot.height),
(1180, 760),
"验收图必须 1180×760 @100% DPI"
);
let light = matches(
shot.at(2, 2),
rgb(ui(SchemeFlavor::Light).color(Role::Surface)),
);
let flavor = if light {
SchemeFlavor::Light
} else {
SchemeFlavor::Dark
};
let ui = ui(flavor);
let surface = ui.color(Role::Surface);
let primary = ui.color(Role::Primary);
let on_primary = ui.color(Role::OnPrimary);
let unselected = ui.color(Role::OnSurfaceVariant);
// ---- §18.1 / §18.3 / §19.1:20 的外缘,选中与未选中**同尺寸** -----------------
// 逐块断言(复审 P2-4):**每一块** Primary 都必须是 20(±1)——任何一块到 22 就说明描边
// 画到了框外。只数「20×20 有几块」的时候,「把其中一格放大成 22」照样绿。
let filled = ink_boxes(&shot, primary, surface, 15, 24);
for bbox in &filled {
assert!(
bbox.width() <= 21 && bbox.height() <= 21,
"18.3:Primary 块不得超过 21(22 = 描边画到框外),实测 {}×{} @ {:?}",
bbox.width(),
bbox.height(),
(bbox.left, bbox.top)
);
}
let twenty: Vec<_> = filled
.iter()
.copied()
.filter(|bbox| near(bbox.width(), 20) && near(bbox.height(), 20))
.collect();
assert_eq!(
twenty.len(),
12,
"18.1:应**恰好** 12 块 20(±1) 的 Primary(checkbox 选中 4 + 部分选中 4 + radio 环 4),\
实际 {}:{}",
twenty.len(),
dims(&filled)
);
// 未选中(OnSurfaceVariant 描边):逐块都必须 20(±1),且内部透底
let candidates = ink_boxes(&shot, unselected, surface, 17, 24);
let rings: Vec<_> = candidates
.iter()
.copied()
.filter(|bbox| !matches(shot.at(center(bbox).0, bbox.top + 3), rgb(unselected)))
.collect();
assert!(
!rings.is_empty(),
"18.2:没找到未选中的复选框;候选:{}",
dims(&candidates)
);
for bbox in &rings {
assert!(
near(bbox.width(), 20) && near(bbox.height(), 20),
"18.3:未选中外缘必须与选中同尺寸 20(描边向内收),实测 {}×{} @ {:?}",
bbox.width(),
bbox.height(),
(bbox.left, bbox.top)
);
assert!(
!matches(shot.at(center(bbox).0, center(bbox).1), rgb(unselected)),
"18.2:未选中框内部必须透出底色"
);
}
// A1:hover/pressed/focused 三列的未选中描边提到 `OnSurface`
let on_surface = ui.color(Role::OnSurface);
let lifted: Vec<_> = ink_boxes(&shot, on_surface, surface, 17, 24)
.into_iter()
.filter(|bbox| !matches(shot.at(center(bbox).0, bbox.top + 3), rgb(on_surface)))
.collect();
assert!(
lifted.len() >= 3,
"A1:未选中复选框在 hover/pressed/focused 的描边应是 OnSurface(3 个),实际 {}",
lifted.len()
);
for bbox in &lifted {
assert!(
near(bbox.width(), 20) && near(bbox.height(), 20),
"18.3:提到 OnSurface 之后外缘仍是 20,实测 {}×{}",
bbox.width(),
bbox.height()
);
}
// ---- §19.2:Radio 内点 **10 不是 12**;§19.6:环是真圆 ------------------------
// 环/实心看**上沿内缩 3px 的点**(环在那里是内孔);不看正中心——短横正好压中心。
let mut dots = 0;
for bbox in &twenty {
if matches(shot.at(center(bbox).0, bbox.top + 3), rgb(primary)) {
continue;
}
assert!(
!matches(shot.at(bbox.left + 1, bbox.top + 1), rgb(primary)),
"19.6:Radio 外圈必须是真圆(不是圆角方)@ {:?}",
(bbox.left, bbox.top)
);
let dot = inner_ink(&shot, bbox, primary, surface, 4).expect("选中的 radio 必须有内点");
assert!(
near(dot.width(), 10) && near(dot.height(), 10),
"19.2:Radio 内点可视直径 10(12 是含描边前的名义值),实测 {}×{}",
dot.width(),
dot.height()
);
assert_ne!(dot.width(), 12, "19.2:照名义值抄 12 会明显偏大");
dots += 1;
}
assert_eq!(
dots, 4,
"19.2:应恰好 4 个选中的 radio(四态),实际 {dots}"
);
// ---- §18.4 / §18.6:勾形墨迹跨度;indeterminate 是**短横**不是勾 --------------
let mut bars = 0;
let mut checks = 0;
for bbox in &twenty {
let Some(glyph) = inner_ink(&shot, bbox, on_primary, primary, 3) else {
continue;
};
if glyph.height() <= 4 {
bars += 1;
assert!(
glyph.width() >= 12,
"18.6:indeterminate 必须是居中短横,实测 {}×{}",
glyph.width(),
glyph.height()
);
} else {
checks += 1;
// 端点 12 + 方头两侧各 2/√2 ≈ 1.41 → 墨迹 ≈14.8(见 `ControlGeometry`)
assert!(
(13..=16).contains(&glyph.width()),
"18.4:勾形墨迹横向应 ≈14.8(端点 12 + 方头外扩),实测 {}",
glyph.width()
);
assert!(
glyph.height() >= 6,
"18.4:勾形有折点,实测高 {}",
glyph.height()
);
}
}
assert!(
bars >= 4 && checks >= 4,
"18.4/18.6:勾 {checks} 个、短横 {bars} 个(各应 ≥4)"
);
// ---- §18.5 / §19.7:状态层直径 40,且**每一块**都是圆(四角无墨)----------------
// hover 是 8%、pressed/focus 是 10%:**两种颜色都要查**。只查 8% 会漏掉「按下那一列被画成
// 方块」——代码 reviewer 的第二次变异正好落在这个盲点上(实测它当时是绿的)。
let mut circles = 0;
for state in [Interaction::Hovered, Interaction::Pressed] {
let layer = interaction::overlay_color(&ui, state, Role::OnSurfaceVariant, Role::Surface)
.expect("可交互态有状态层");
let found: Vec<_> = ink_boxes(&shot, layer, surface, 37, 43)
.into_iter()
.filter(|bbox| near(bbox.width(), 40) && near(bbox.height(), 40))
.collect();
assert!(
found.len() >= 2,
"18.5:{state:?} 的 Ø40 状态层应 ≥2 块(checkbox×3 + radio + switch),实测 {}:{}",
found.len(),
dims(&found)
);
for circle in &found {
for (x, y) in [
(circle.left, circle.top),
(circle.right, circle.top),
(circle.left, circle.bottom),
(circle.right, circle.bottom),
] {
assert!(
!matches(shot.at(x, y), rgb(layer)),
"18.5:{state:?} 状态层必须是圆(外接框四角不该有墨)@ ({x},{y})"
);
}
}
circles += found.len();
}
// ---- §10.1 Switch 轨道 52×32 · §13.1 Chip 高 32 -------------------------------
let on_tracks: Vec<_> = ink_boxes(&shot, primary, surface, 28, 57)
.into_iter()
.filter(|bbox| near(bbox.width(), 52) && near(bbox.height(), 32))
.collect();
assert_eq!(
on_tracks.len(),
1,
"10.1:选中的 switch 轨道应**恰好一块** 52×32,实测 {}:{}",
on_tracks.len(),
dims(&on_tracks)
);
let chips: Vec<_> = ink_boxes(&shot, ui.color(Role::SecondaryContainer), surface, 20, 140)
.into_iter()
.filter(|bbox| near(bbox.height(), 32))
.collect();
// 选中的 chip 在**四态**里容器都是 `SecondaryContainer`(状态层只叠在未选中的透明底上),
// 所以这里应恰好 4 块——多于 4 说明别的组件的容器也是这个角色、且高 32。
assert_eq!(
chips.len(),
4,
"13.1:选中的 filter chip 应**恰好四块**(enabled/hover/pressed/focused 各一,32 高),\
实测 {}:{}",
chips.len(),
dims(&chips)
);
assert!(
chips[0].width() >= 58,
"13.1:chip 宽应 ≥58(图标 16 + 两侧各 8 + 文字),实测 {}",
chips[0].width()
);
// ---- 复审 P2-1:禁用的 chip **只降不删**(选中态与图标都要在)------------------
// 判据:禁用列里必须**恰好有一块** 12% 填充的 chip(= filter·on)。旧 API 把 selected
// 一起删掉,这一块根本不存在(实测 0 块);能查到它、且宽度 ≥58,说明选中态与图标都还在。
// (未选中的那三个禁用 chip 只有 1px 描边,落在半像素上时覆盖率不足 50%,量不稳,故不在此断言。)
let disabled_container = interaction::disabled_colors(&ui, Role::Surface).0;
let disabled_chips: Vec<_> = ink_boxes(&shot, disabled_container, surface, 28, 140)
.into_iter()
.filter(|bbox| near(bbox.height(), 32) && bbox.width() >= 58)
.collect();
assert_eq!(
disabled_chips.len(),
1,
"P2-1:禁用列应**恰好一块**填充 chip(filter·on 的选中态不能被删掉),实测 {}:{}",
disabled_chips.len(),
dims(&disabled_chips)
);
assert!(
disabled_chips[0].width() >= 58,
"P2-1:禁用 chip 宽度应 ≥58(图标还在),实测 {}",
disabled_chips[0].width()
);
// ---- 复审 P2-3:**选中且禁用**的 Checkbox 容器是 38%,不是 12% -----------------
let disabled_ink = interaction::disabled_content_over_role(&ui, Role::OnSurface, false);
let ink38: Vec<_> = ink_boxes(&shot, disabled_ink, surface, 17, 24)
.into_iter()
.filter(|bbox| near(bbox.width(), 20) && near(bbox.height(), 20))
.collect();
assert!(
ink38.len() >= 3,
"18.8:禁用列应有 ≥3 块 20(±1) 的 38% 叠色(选中禁用填充 + 未选中禁用描边 + radio 环),\
实测 {}:{}",
ink38.len(),
dims(&ink38)
);
let container_20: Vec<_> = ink_boxes(&shot, disabled_container, surface, 19, 21)
.into_iter()
.filter(|bbox| near(bbox.width(), 20) && near(bbox.height(), 20))
.collect();
assert!(
container_20.is_empty(),
"P2-3:20×20 的禁用块不许是 12% 容器色(那是普通禁用容器,选中禁用是 38%),实测 {}",
dims(&container_20)
);
eprintln!(
"[selection-probe] OK flavor={flavor:?}:20(±1) Primary 块 {} · 未选中环 {} · 内点 {dots} · \
勾 {checks} / 短横 {bars} · 状态层 {} · 禁用 38% {} · 禁用 chip {}",
twenty.len(),
rings.len(),
circles,
ink38.len(),
disabled_chips.len()
);
}