From 948a3685cd18e5775ce3c2c3486f6c5fbaddae40 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 09:58:44 +0800 Subject: [PATCH] =?UTF-8?q?test(design):=20=E9=92=89=E4=BD=8F=20D40=20?= =?UTF-8?q?=E7=9A=84=E5=BD=A9=E5=BA=A6=E9=97=A8=E6=A7=9B=208=EF=BC=88?= =?UTF-8?q?=E5=A4=8D=E5=AE=A1=20r1=20p2-3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MIN_SEED_CHROMA 8.0 → 6.0 的变异原本全绿(那条断言比的是常量自己)。 新增两条用实测 chroma≈7.23 的灰蓝(#7E8489): - seed::tests::chroma_just_below_the_gate_is_rejected(门槛之上/之下夹住) - resolve::tests::faint_colour_falls_back_to_the_selected_card(回落 + 落点是选中卡 + 方案逐值等于该卡) 顺带把 resolve 的单测拆到 resolve/tests.rs(400 行上限) --- crates/design/src/palette/resolve/mod.rs | 174 +----------------- crates/design/src/palette/resolve/tests.rs | 204 +++++++++++++++++++++ crates/design/src/palette/seed/tests.rs | 28 +++ 3 files changed, 233 insertions(+), 173 deletions(-) create mode 100644 crates/design/src/palette/resolve/tests.rs diff --git a/crates/design/src/palette/resolve/mod.rs b/crates/design/src/palette/resolve/mod.rs index f88ef44..f8c328e 100644 --- a/crates/design/src/palette/resolve/mod.rs +++ b/crates/design/src/palette/resolve/mod.rs @@ -196,176 +196,4 @@ fn card_key(card: Card) -> CardKey { } #[cfg(test)] -mod tests { - use super::*; - use crate::color::Color; - - fn solid(color: Color, width: u32, height: u32) -> Vec { - let mut out = Vec::with_capacity((width * height * 4) as usize); - for _ in 0..(width * height) { - out.extend_from_slice(&[color.r, color.g, color.b, color.a]); - } - out - } - - #[test] - fn level_one_wins_when_background_is_good() { - let pixels = solid(Color::rgb(0xC0, 0x39, 0x2B), 4, 4); - let (scheme, provenance) = resolve_source( - Some(ImageInput::pixels(&pixels, 4, 4)), - None, - Some(CardKey::Forest), - SchemeFlavor::Light, - ); - assert_eq!(provenance.level, Level::BackgroundImage); - assert_eq!(provenance.background(), Outcome::Used); - assert_eq!(provenance.preset(), Outcome::Absent); - assert!(provenance.hint_key().is_none()); - assert!(check(&scheme).is_clean()); - assert!(provenance.seed.is_some()); - // 取色方案的表面色温来自当前色卡(森林绿),不是默认卡。 - assert_eq!(provenance.card, CardKey::Forest); - } - - #[test] - fn gray_background_falls_back_to_wallpaper_with_reason() { - // 反证 1:灰白背景图 → 不许凭空上色,必须带着 low-chroma 原因回落。 - let gray = solid(Color::rgb(0xF2, 0xF2, 0xF0), 8, 8); - let teal = solid(Color::rgb(0x00, 0x79, 0x6B), 8, 8); - let (_, provenance) = resolve_source( - Some(ImageInput::pixels(&gray, 8, 8)), - Some(ImageInput::pixels(&teal, 8, 8)), - Some(CardKey::Sky), - SchemeFlavor::Light, - ); - assert_eq!(provenance.level, Level::Wallpaper); - match provenance.background() { - Outcome::Failed(Skip::LowChroma { chroma, .. }) => assert!(chroma < 8.0), - other => panic!("背景图应当是低彩度失败,实际 {other:?}"), - } - assert_eq!( - provenance.hint_key(), - Some("palette.hint.background.low-chroma") - ); - assert!(provenance.did_fall_back()); - assert_eq!( - provenance.reason_codes(), - vec![("background-image", "low-chroma")] - ); - } - - #[test] - fn undecodable_and_tiny_inputs_are_distinguished_from_absent() { - let tiny = solid(Color::rgb(0xFF, 0x00, 0x00), 1, 2); - let (_, provenance) = resolve_source( - Some(ImageInput::Undecodable), - Some(ImageInput::pixels(&tiny, 1, 2)), - None, - SchemeFlavor::Dark, - ); - assert_eq!(provenance.level, Level::Preset); - assert_eq!(provenance.background(), Outcome::Failed(Skip::Undecodable)); - assert_eq!( - provenance.wallpaper(), - Outcome::Failed(Skip::TooSmall { - width: 1, - height: 2 - }) - ); - assert_eq!( - provenance.hint_key(), - Some("palette.hint.background.undecodable") - ); - // 从没选过卡 → 出厂默认。 - assert_eq!(provenance.card, CardKey::Sky); - } - - #[test] - fn nothing_provided_uses_default_card_without_hint() { - let (scheme, provenance) = resolve_source(None, None, None, SchemeFlavor::Light); - assert_eq!(provenance.level, Level::Preset); - assert_eq!(provenance.preset(), Outcome::Used); - assert!(!provenance.did_fall_back()); - assert!(provenance.adjustments.is_empty()); - assert!(provenance.hint_key().is_none()); - assert_eq!(scheme, DEFAULT_CARD.scheme(SchemeFlavor::Light)); - } - - #[test] - fn preset_selection_decides_the_fallback_colour() { - // 反证 2:回落落点是「用户选的那张卡」,不是默认卡。 - let gray = solid(Color::rgb(0x80, 0x80, 0x80), 4, 4); - let (scheme, provenance) = resolve_source( - Some(ImageInput::pixels(&gray, 4, 4)), - None, - Some(CardKey::Rose), - SchemeFlavor::Light, - ); - assert_eq!(provenance.level, Level::Preset); - assert_eq!(provenance.card, CardKey::Rose); - assert_eq!(scheme, CardKey::Rose.card().scheme(SchemeFlavor::Light)); - } - - #[test] - fn background_beats_wallpaper_when_both_work() { - let bg = solid(Color::rgb(0xB2, 0x6A, 0x00), 8, 8); - let wp = solid(Color::rgb(0x00, 0x79, 0x6B), 8, 8); - let (_, provenance) = resolve_source( - Some(ImageInput::pixels(&bg, 8, 8)), - Some(ImageInput::pixels(&wp, 8, 8)), - Some(CardKey::Teal), - SchemeFlavor::Dark, - ); - assert_eq!(provenance.level, Level::BackgroundImage); - assert_eq!(provenance.wallpaper(), Outcome::Absent, "② 根本没轮到"); - assert!(!provenance.did_fall_back()); - } - - #[test] - fn transparent_background_reports_its_own_reason() { - let transparent = solid(Color::rgba(0x3C, 0x8D, 0xBC, 0), 4, 4); - let (_, provenance) = resolve_source( - Some(ImageInput::pixels(&transparent, 4, 4)), - None, - None, - SchemeFlavor::Light, - ); - assert_eq!( - provenance.background(), - Outcome::Failed(Skip::NoOpaquePixels) - ); - assert_eq!( - provenance.hint_key(), - Some("palette.hint.background.no-opaque-pixels") - ); - } - - #[test] - fn a_five_percent_accent_drives_level_one() { - // 端到端反证(与 `palette::seed::tests::end_to_end_picks_the_five_percent_accent` 配对): - // 95% 米白 + 5% 珊瑚的背景图**必须**在一级取色成功,且种子里就是那 5% 的珊瑚。 - // 若 `analyze_rgba` 退化成「不透明像素求算术平均」,这条会变成「回落色卡」而红。 - let beige = Color::rgb(0xF5, 0xF1, 0xEA); - let coral = Color::rgb(0xFF, 0x6B, 0x5A); - let mut pixels = Vec::with_capacity(40 * 40 * 4); - for index in 0..40 * 40 { - let color = if index < 40 * 40 - 80 { beige } else { coral }; - pixels.extend_from_slice(&[color.r, color.g, color.b, color.a]); - } - - let (scheme, provenance) = resolve_source( - Some(ImageInput::pixels(&pixels, 40, 40)), - None, - Some(CardKey::Rose), - SchemeFlavor::Light, - ); - assert_eq!(provenance.level, Level::BackgroundImage, "{provenance:?}"); - assert_eq!(provenance.background(), Outcome::Used); - let seed = provenance.seed.expect("一级成功必须带种子"); - assert_eq!(seed.color, coral, "种子应当是 5% 的珊瑚"); - assert!(provenance.hint_key().is_none()); - // 方案来自珊瑚,而不是「回落到用户选的 Rose 卡」——两者 primary 不同。 - assert_ne!(scheme, CardKey::Rose.card().scheme(SchemeFlavor::Light)); - assert!(crate::palette::wcag::check(&scheme).is_clean()); - } -} +mod tests; diff --git a/crates/design/src/palette/resolve/tests.rs b/crates/design/src/palette/resolve/tests.rs new file mode 100644 index 0000000..ee34e38 --- /dev/null +++ b/crates/design/src/palette/resolve/tests.rs @@ -0,0 +1,204 @@ +//! `resolve` 的单测:三级优先序、失败回落、以及「回落落点是用户选的卡」这三类断言。 +//! +//! 与 `mod.rs` 拆开只为单文件 ≤400 行;测试仍属于 `palette::resolve::tests`。 + +use super::*; +use crate::color::Color; + +fn solid(color: Color, width: u32, height: u32) -> Vec { + let mut out = Vec::with_capacity((width * height * 4) as usize); + for _ in 0..(width * height) { + out.extend_from_slice(&[color.r, color.g, color.b, color.a]); + } + out +} + +#[test] +fn level_one_wins_when_background_is_good() { + let pixels = solid(Color::rgb(0xC0, 0x39, 0x2B), 4, 4); + let (scheme, provenance) = resolve_source( + Some(ImageInput::pixels(&pixels, 4, 4)), + None, + Some(CardKey::Forest), + SchemeFlavor::Light, + ); + assert_eq!(provenance.level, Level::BackgroundImage); + assert_eq!(provenance.background(), Outcome::Used); + assert_eq!(provenance.preset(), Outcome::Absent); + assert!(provenance.hint_key().is_none()); + assert!(check(&scheme).is_clean()); + assert!(provenance.seed.is_some()); + // 取色方案的表面色温来自当前色卡(森林绿),不是默认卡。 + assert_eq!(provenance.card, CardKey::Forest); +} + +#[test] +fn gray_background_falls_back_to_wallpaper_with_reason() { + // 反证 1:灰白背景图 → 不许凭空上色,必须带着 low-chroma 原因回落。 + let gray = solid(Color::rgb(0xF2, 0xF2, 0xF0), 8, 8); + let teal = solid(Color::rgb(0x00, 0x79, 0x6B), 8, 8); + let (_, provenance) = resolve_source( + Some(ImageInput::pixels(&gray, 8, 8)), + Some(ImageInput::pixels(&teal, 8, 8)), + Some(CardKey::Sky), + SchemeFlavor::Light, + ); + assert_eq!(provenance.level, Level::Wallpaper); + match provenance.background() { + Outcome::Failed(Skip::LowChroma { chroma, .. }) => assert!(chroma < 8.0), + other => panic!("背景图应当是低彩度失败,实际 {other:?}"), + } + assert_eq!( + provenance.hint_key(), + Some("palette.hint.background.low-chroma") + ); + assert!(provenance.did_fall_back()); + assert_eq!( + provenance.reason_codes(), + vec![("background-image", "low-chroma")] + ); +} + +#[test] +fn undecodable_and_tiny_inputs_are_distinguished_from_absent() { + let tiny = solid(Color::rgb(0xFF, 0x00, 0x00), 1, 2); + let (_, provenance) = resolve_source( + Some(ImageInput::Undecodable), + Some(ImageInput::pixels(&tiny, 1, 2)), + None, + SchemeFlavor::Dark, + ); + assert_eq!(provenance.level, Level::Preset); + assert_eq!(provenance.background(), Outcome::Failed(Skip::Undecodable)); + assert_eq!( + provenance.wallpaper(), + Outcome::Failed(Skip::TooSmall { + width: 1, + height: 2 + }) + ); + assert_eq!( + provenance.hint_key(), + Some("palette.hint.background.undecodable") + ); + // 从没选过卡 → 出厂默认。 + assert_eq!(provenance.card, CardKey::Sky); +} + +#[test] +fn nothing_provided_uses_default_card_without_hint() { + let (scheme, provenance) = resolve_source(None, None, None, SchemeFlavor::Light); + assert_eq!(provenance.level, Level::Preset); + assert_eq!(provenance.preset(), Outcome::Used); + assert!(!provenance.did_fall_back()); + assert!(provenance.adjustments.is_empty()); + assert!(provenance.hint_key().is_none()); + assert_eq!(scheme, DEFAULT_CARD.scheme(SchemeFlavor::Light)); +} + +#[test] +fn preset_selection_decides_the_fallback_colour() { + // 反证 2:回落落点是「用户选的那张卡」,不是默认卡。 + let gray = solid(Color::rgb(0x80, 0x80, 0x80), 4, 4); + let (scheme, provenance) = resolve_source( + Some(ImageInput::pixels(&gray, 4, 4)), + None, + Some(CardKey::Rose), + SchemeFlavor::Light, + ); + assert_eq!(provenance.level, Level::Preset); + assert_eq!(provenance.card, CardKey::Rose); + assert_eq!(scheme, CardKey::Rose.card().scheme(SchemeFlavor::Light)); +} + +#[test] +fn background_beats_wallpaper_when_both_work() { + let bg = solid(Color::rgb(0xB2, 0x6A, 0x00), 8, 8); + let wp = solid(Color::rgb(0x00, 0x79, 0x6B), 8, 8); + let (_, provenance) = resolve_source( + Some(ImageInput::pixels(&bg, 8, 8)), + Some(ImageInput::pixels(&wp, 8, 8)), + Some(CardKey::Teal), + SchemeFlavor::Dark, + ); + assert_eq!(provenance.level, Level::BackgroundImage); + assert_eq!(provenance.wallpaper(), Outcome::Absent, "② 根本没轮到"); + assert!(!provenance.did_fall_back()); +} + +#[test] +fn transparent_background_reports_its_own_reason() { + let transparent = solid(Color::rgba(0x3C, 0x8D, 0xBC, 0), 4, 4); + let (_, provenance) = resolve_source( + Some(ImageInput::pixels(&transparent, 4, 4)), + None, + None, + SchemeFlavor::Light, + ); + assert_eq!( + provenance.background(), + Outcome::Failed(Skip::NoOpaquePixels) + ); + assert_eq!( + provenance.hint_key(), + Some("palette.hint.background.no-opaque-pixels") + ); +} + +#[test] +fn faint_colour_falls_back_to_the_selected_card() { + // 复审 p2-3:D40 的门槛是 8,落在 6~8 之间的图必须**回落**,且落点是用户选中的那张卡。 + // 变异反证:`MIN_SEED_CHROMA` 8.0 → 6.0 时,这个种子会通过门槛, + // `level` 变成 `BackgroundImage`,本条的第一条断言变红。 + let faint = Color::rgb(0x7E, 0x84, 0x89); // CAM16 chroma ≈ 7.23 + let pixels = solid(faint, 16, 16); + let (scheme, provenance) = resolve_source( + Some(ImageInput::pixels(&pixels, 16, 16)), + None, + Some(CardKey::Amber), + SchemeFlavor::Light, + ); + + assert_eq!(provenance.level, Level::Preset, "低彩度图必须回落"); + assert_eq!(provenance.card, CardKey::Amber, "落点是用户选中的卡"); + assert_eq!( + scheme, + CardKey::Amber.card().scheme(SchemeFlavor::Light), + "方案必须逐值等于选中卡" + ); + match provenance.background() { + Outcome::Failed(Skip::LowChroma { chroma, .. }) => { + assert!((6.0..8.0).contains(&chroma), "实测彩度 {chroma}"); + } + other => panic!("应当是低彩度失败,实际 {other:?}"), + } +} + +#[test] +fn a_five_percent_accent_drives_level_one() { + // 端到端反证(与 `palette::seed::tests::end_to_end_picks_the_five_percent_accent` 配对): + // 95% 米白 + 5% 珊瑚的背景图**必须**在一级取色成功,且种子里就是那 5% 的珊瑚。 + // 若 `analyze_rgba` 退化成「不透明像素求算术平均」,这条会变成「回落色卡」而红。 + let beige = Color::rgb(0xF5, 0xF1, 0xEA); + let coral = Color::rgb(0xFF, 0x6B, 0x5A); + let mut pixels = Vec::with_capacity(40 * 40 * 4); + for index in 0..40 * 40 { + let color = if index < 40 * 40 - 80 { beige } else { coral }; + pixels.extend_from_slice(&[color.r, color.g, color.b, color.a]); + } + + let (scheme, provenance) = resolve_source( + Some(ImageInput::pixels(&pixels, 40, 40)), + None, + Some(CardKey::Rose), + SchemeFlavor::Light, + ); + assert_eq!(provenance.level, Level::BackgroundImage, "{provenance:?}"); + assert_eq!(provenance.background(), Outcome::Used); + let seed = provenance.seed.expect("一级成功必须带种子"); + assert_eq!(seed.color, coral, "种子应当是 5% 的珊瑚"); + assert!(provenance.hint_key().is_none()); + // 方案来自珊瑚,而不是「回落到用户选的 Rose 卡」——两者 primary 不同。 + assert_ne!(scheme, CardKey::Rose.card().scheme(SchemeFlavor::Light)); + assert!(crate::palette::wcag::check(&scheme).is_clean()); +} diff --git a/crates/design/src/palette/seed/tests.rs b/crates/design/src/palette/seed/tests.rs index fc1c598..1a65375 100644 --- a/crates/design/src/palette/seed/tests.rs +++ b/crates/design/src/palette/seed/tests.rs @@ -224,3 +224,31 @@ fn seed_scheme_meets_wcag_on_the_primary_pair() { assert!(ratio >= 4.5, "{flavor:?} 只有 {ratio}"); } } + +#[test] +fn chroma_just_below_the_gate_is_rejected() { + // 把 D40 的口径(`MIN_SEED_CHROMA` = 8,「< ~8 视为无信息量」)钉成可执行的断言: + // 图里的颜色实测 chroma ≈7.23 —— 落在 `Score` 的候选门槛(5)之上、D40 的种子门槛(8)之下。 + // 这类图必须**回落**,而不是「勉强取一个低彩度种子」。 + // + // 变异反证:把 `MIN_SEED_CHROMA` 从 8.0 改成 6.0,这个种子会通过门槛 → 本条变红。 + let faint = Color::from_hex("#7E8489").expect("合法 hex"); + let chroma = crate::color::Hct::from_color(faint).chroma; + assert!( + (6.0..8.0).contains(&chroma), + "这条测试依赖「彩度落在 6~8 之间」,实测 {chroma}" + ); + + let pixels = solid(faint, 16, 16); + match analyze_rgba(&pixels, 16, 16) { + Err(Failure::LowChroma { + chroma: measured, .. + }) => { + assert!( + (6.0..MIN_SEED_CHROMA).contains(&measured), + "报回来的彩度 {measured} 应当仍在门槛之下" + ); + } + other => panic!("彩度 {chroma} 必须回落,实际 {other:?}"), + } +}