test(design): 新增走 resolve_source 的接线守护测试(复审 r1 p1-1)+ 两条小修
**p1-1**:新增 `resolved_image_scheme_keeps_tinting_from_the_seed`——走产品公开入口
`resolve_source(Some(ImageInput::pixels(..)), None, Some(CardKey::Sky/Teal), Light)`,断言
`provenance.level == Level::BackgroundImage`、`surface == #FFF8F7`(暖白,跟着图走)、色相
≈22°(远离固定的 239.51°)、两张卡下主色与底色都相同、且与天蓝卡的 surface 不同。
上一版只有 `image_scheme_ignores_the_preset_neutral_hue`,它测的是 `Seed::scheme` 生成器层:
把 `resolve::try_level` 的取色分支改成固定中性时 8 条测试全绿(复审已实证)。那条保留并补了
⚠️ 注明「不覆盖接线」,避免下次又把生成器层当成接线守护。
**小修 1**:`every_card_scheme_meets_wcag_aa_under_the_fixed_neutral` 的 on-surface/surface
量级断言按 `flavor` 分开(原来 `(16.30..=16.40) || (14.28..=14.36)`,浅/深两值互换也能过)。
**小修 2**:测试文件拆分以守住 ≤400 行——`tests/card_neutral.rs`(crate 根,400 行)+
`tests/card_neutral/common.rs`(改前路径复原 / 模式列表 / 色相环距离,27 行)+
`tests/card_neutral/dump.rs`(出图数据导出 + 改前/改后实测表,68 行,无断言)。crate 根的子模块
默认在 `tests/` 下找,所以用 `#[path]` 指明目录。
`cargo test -p unlucky-design --test card_neutral` → 9 passed(8 + 新 1 条)。
This commit is contained in:
1 parent
4b059201fd
commit
13248ddc9b
3 files changed
+185
-89
No files matched your search
@@ -1,30 +1,17 @@
|
||||
//! D43:内置色卡的**固定中性色相**——切卡只换强调色,窗口底色的冷暖不跟着换。
|
||||
//!
|
||||
//! ## 这条测试要钉住的四件事
|
||||
//! ## 这条测试要钉住的五件事
|
||||
//!
|
||||
//! 1. **天蓝卡(出厂默认)逐值不变**:固定的中性色相等于天蓝档的中性色相,所以天蓝卡必须与
|
||||
//! [`Scheme::baseline`] 的 49 个角色**逐个相等**(M70 定案的不动点,p1 级硬约束)。
|
||||
//! 2. **八张卡的表面色完全相同**:`surface` 的 CAM16 色相一致(±1°)、实测彩度落在窄带内
|
||||
//! (±1.5),且改前那条「9 倍差」被同时量出来当反证(见 `the_old_per_card_tinting...`)。
|
||||
//! 3. **动态路径不许被顺手改成固定中性**:同一张图在两张不同卡下必须生成**同一套强调色**,
|
||||
//! 而底色由图像种子的色相决定(Monet 体验,D38)。
|
||||
//! 3. **动态路径不许被顺手改成固定中性**:这条必须走 **`resolve_source` 公开入口**测——
|
||||
//! `Card::scheme` 那一层根本没有「取色」这回事,测它等于测空气(复审 r1 p1-1 就是这个:
|
||||
//! 把 `resolve::try_level` 的取色分支改成固定中性,8 条测试全绿)。见
|
||||
//! `resolved_image_scheme_keeps_tinting_from_the_seed`。
|
||||
//! 4. **WCAG 不因换中性色相而掉档**:8 张卡 × 浅/深两套 × 26 对文字/图标对全部 ≥ 4.5:1。
|
||||
//!
|
||||
//! ## 实测表(2026-11,本文件 `dump_card_palette_for_the_color_card` 的输出)
|
||||
//!
|
||||
//! | 卡 | 改前 `surface`(浅) | 改后 `surface`(浅) | 改前 HCT(浅) | 改后 HCT(浅) |
|
||||
//! |---|---|---|---|---|
|
||||
//! | sky | `#F6FAFF` | `#F6FAFF` | 236.86° / 6.41 | 236.86° / 6.41 |
|
||||
//! | lavender | `#FDF7FF` | `#F6FAFF` | 298.10° / 5.87 | 236.86° / 6.41 |
|
||||
//! | forest | `#EEFDEE` | `#F6FAFF` | 157.01° / 11.77 | 236.86° / 6.41 |
|
||||
//! | amber | `#FFF8F4` | `#F6FAFF` | 71.48° / 1.89 | 236.86° / 6.41 |
|
||||
//! | rose | `#FFF8F7` | `#F6FAFF` | 22.09° / 1.29 | 236.86° / 6.41 |
|
||||
//! | teal | `#EBFEF8` | `#F6FAFF` | 182.42° / 12.03 | 236.86° / 6.41 |
|
||||
//! | dusk | `#FBF8FF` | `#F6FAFF` | 280.31° / 5.44 | 236.86° / 6.41 |
|
||||
//! | olive | `#F9FBE1` | `#F6FAFF` | 120.99° / 11.84 | 236.86° / 6.41 |
|
||||
//!
|
||||
//! 深色档同理:改前 `#09151C`~`#1F0F10`、彩度 11.63~12.45,改后八张卡统一 `#09151C`
|
||||
//! (235.70° / 11.70)。
|
||||
//! 5. 想让「八卡同底色」的带宽松掉,`the_old_per_card_tinting...` 会红(它量的是改前路径)。
|
||||
//!
|
||||
//! ## 为什么容差是这个带宽(不是随手取整)
|
||||
//!
|
||||
@@ -36,35 +23,24 @@
|
||||
//! 真正的危险是暖相回退——rose 1.29、amber 1.89,比基线低 4.5~5.1,远超 1.5 的带宽;
|
||||
//! 紫相(lavender 5.87)只低 0.54,靠上面的色相断言抓(298° 偏 61°)。带宽取 1.5 = 请求
|
||||
//! 彩度的 1/8,比「改前最小的一张卡偏离基线」还小,所以「9 倍差回来了」一定红。
|
||||
//!
|
||||
//! 改前/改后的 `surface` 实测表在子模块 [`dump`](那里同时是验收图的数据源)。
|
||||
|
||||
// 子模块放在 `tests/card_neutral/` 目录里(cargo 只把 `tests/*.rs` 顶层文件当一个 target,
|
||||
// 目录里的文件不会被当成独立 target;crate 根的子模块默认在 `tests/` 下找,所以显式 `#[path]`)。
|
||||
#[path = "card_neutral/common.rs"]
|
||||
mod common;
|
||||
#[path = "card_neutral/dump.rs"]
|
||||
mod dump;
|
||||
|
||||
use common::{before_d43, flavors, hue_gap};
|
||||
use unlucky_design::color::{
|
||||
Hct, Role, Scheme, SchemeFlavor, DEFAULT_NEUTRAL_CHROMA, NEUTRAL_HUE_DEG,
|
||||
};
|
||||
use unlucky_design::palette::card::card_scheme_is_readable;
|
||||
use unlucky_design::palette::{Card, CardKey, CARDS, DEFAULT_CARD, TEXT_PAIRS};
|
||||
|
||||
/// 改前的路径(M87 及之前):中性调色板跟着种子染色(官方 TonalSpot)。
|
||||
///
|
||||
/// 产品代码已经不用它了,但公开 API 仍在(取色路径就靠它逐色染色),所以这里能原样复现「改前」。
|
||||
fn before_d43(card: Card, flavor: SchemeFlavor) -> Scheme {
|
||||
Scheme::from_palettes(
|
||||
&unlucky_design::color::Palettes::from_seed_with_neutral_chroma(
|
||||
card.seed,
|
||||
card.neutral_chroma,
|
||||
),
|
||||
flavor,
|
||||
)
|
||||
}
|
||||
|
||||
fn flavors() -> [SchemeFlavor; 2] {
|
||||
[SchemeFlavor::Light, SchemeFlavor::Dark]
|
||||
}
|
||||
|
||||
/// 色相环上的最短距离(度)。
|
||||
fn hue_gap(a: f64, b: f64) -> f64 {
|
||||
let raw = (a - b).abs();
|
||||
raw.min(360.0 - raw)
|
||||
}
|
||||
use unlucky_design::palette::{
|
||||
resolve_source, Card, CardKey, ImageInput, Level, CARDS, DEFAULT_CARD, TEXT_PAIRS,
|
||||
};
|
||||
|
||||
/// 天蓝卡就是产品默认色:固定的中性色相**沿用**它,所以 49 个角色必须一位不差。
|
||||
///
|
||||
@@ -274,10 +250,11 @@ fn card_scheme_matches_from_accent_when_chroma_is_the_default() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 动态路径的守护测试(D38/D43):同一张图,**两张不同色卡下强调色相同、底色由图像 seed 决定**。
|
||||
/// **生成器层**:同一张图,两张不同色卡下强调色相同、底色由图像 seed 决定。
|
||||
///
|
||||
/// 谁把取色路径也改成固定中性,这条立刻红:那时两张卡的 `surface` 会变成同一个冷白色,
|
||||
/// 而图像种子的色相(红相 ~22°)会从表面色上消失。
|
||||
/// ⚠️ 只测 `Seed::scheme` 这一层,**不覆盖接线**:取色分支在实际入口
|
||||
/// `palette::resolve::try_level` 里,接线由 `resolved_image_scheme_keeps_tinting_from_the_seed`
|
||||
/// 守(复审 r1 p1-1 的病根就是这条用例看着像守护、其实测不到接线)。
|
||||
#[test]
|
||||
fn image_scheme_ignores_the_preset_neutral_hue() {
|
||||
// 4×4 纯 `#B23A48`(rose 卡的种子色)——色相远离固定的 239.5°,好坏都能分辨。
|
||||
@@ -330,6 +307,65 @@ fn image_scheme_ignores_the_preset_neutral_hue() {
|
||||
}
|
||||
}
|
||||
|
||||
/// **接线的**守护测试(复审 r1 p1-1):走产品公开入口 [`resolve_source`],不是 `Card::scheme`。
|
||||
///
|
||||
/// 上一版只有生成器层那条用例:实测把 `palette::resolve::try_level` 的取色分支改成固定中性
|
||||
/// 时**8 条测试全绿**,而同一张 `#B23A48` 图的 `surface` 已从 `#FFF8F7`(暖白,跟着图走)
|
||||
/// 退化成 `#F6FAFF`(色卡冷白)。断言测 A 层、接线在 B 层——这条把整条链跑通。
|
||||
#[test]
|
||||
fn resolved_image_scheme_keeps_tinting_from_the_seed() {
|
||||
// 4×4 纯 `#B23A48`:种子色相 15.5°(底色 8 bit 量化后读回 22.09°),离 239.51° 很远。
|
||||
let pixels = [0xB2u8, 0x3A, 0x48, 0xFF].repeat(16);
|
||||
let flavor = SchemeFlavor::Light;
|
||||
let mut resolved: Vec<(CardKey, Scheme)> = Vec::new();
|
||||
for key in [CardKey::Sky, CardKey::Teal] {
|
||||
let (scheme, provenance) = resolve_source(
|
||||
Some(ImageInput::pixels(&pixels, 4, 4)),
|
||||
None,
|
||||
Some(key),
|
||||
flavor,
|
||||
);
|
||||
assert_eq!(
|
||||
provenance.level,
|
||||
Level::BackgroundImage,
|
||||
"{key:?}:背景图这一级没被用上(回落链本身变了)"
|
||||
);
|
||||
resolved.push((key, scheme));
|
||||
}
|
||||
|
||||
let surface = resolved[0].1.get(Role::Surface);
|
||||
// ① 底色在按图染色:必须是暖白,而不是色卡那条冷调中性。这一格就是「变异检测器」。
|
||||
assert_eq!(
|
||||
surface.to_hex(),
|
||||
"#FFF8F7",
|
||||
"取色方案的 surface 被拉回了色卡中性色相"
|
||||
);
|
||||
let surface_hue = Hct::from_color(surface).hue;
|
||||
assert!(
|
||||
hue_gap(surface_hue, 22.0) < 5.0,
|
||||
"surface 色相 {surface_hue:.2}° 没跟着图走"
|
||||
);
|
||||
assert!(
|
||||
hue_gap(surface_hue, NEUTRAL_HUE_DEG) > 100.0,
|
||||
"surface 色相 {surface_hue:.2}° 贴到了色卡固定的 {NEUTRAL_HUE_DEG:.2}°"
|
||||
);
|
||||
// ② 两张卡下强调色与底色都相同:底色只由图像决定,色卡只贡献中性彩度。
|
||||
assert_eq!(
|
||||
(resolved[0].1.get(Role::Primary), surface),
|
||||
(
|
||||
resolved[1].1.get(Role::Primary),
|
||||
resolved[1].1.get(Role::Surface)
|
||||
),
|
||||
"同一张图在两张卡下的主色与底色都该相同(八张卡的中性彩度都是 12)"
|
||||
);
|
||||
// ③ 与色卡路径明确不同:否则「取色」就等于「色卡 + 换强调色」。
|
||||
assert_ne!(
|
||||
surface,
|
||||
DEFAULT_CARD.scheme(flavor).get(Role::Surface),
|
||||
"取色方案的 surface 与天蓝卡的相同——取色路径退化成色卡路径了"
|
||||
);
|
||||
}
|
||||
|
||||
/// 8 张卡 × 浅/深 × 26 对:换中性色相后仍全部过 WCAG AA(`wcag::check` 全表),
|
||||
/// 并把最受中性色相影响的一对(on-surface / surface)钉在实测量级上。
|
||||
#[test]
|
||||
@@ -346,54 +382,19 @@ fn every_card_scheme_meets_wcag_aa_under_the_fixed_neutral() {
|
||||
);
|
||||
// 中性色相变了,最受影响的一对是 on-surface / surface(改前逐卡 16.23~16.44 浅 /
|
||||
// 14.30~14.42 深)。改后八张卡同一个底色,这一对也逐卡相同,钉住量级免得漂移。
|
||||
// 区间按模式分开:写成 `(浅区间) || (深区间)` 会让浅/深两个值互换也照样绿。
|
||||
let body = scheme.contrast(Role::OnSurface, Role::Surface);
|
||||
let band = match flavor {
|
||||
SchemeFlavor::Light => 16.30..=16.40,
|
||||
SchemeFlavor::Dark => 14.28..=14.36,
|
||||
};
|
||||
assert!(
|
||||
(16.30..=16.40).contains(&body) || (14.28..=14.36).contains(&body),
|
||||
"{} / {}:on-surface/surface = {body:.3},超出实测区间 \
|
||||
(浅 16.30~16.40 / 深 14.28~14.36)",
|
||||
band.contains(&body),
|
||||
"{} / {}:on-surface/surface = {body:.3},超出 {} 模式的实测区间 {band:?}",
|
||||
card.key,
|
||||
flavor.name(),
|
||||
flavor.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 出图脚本读的 hex 行(改前 + 改后并排),跑法:
|
||||
/// `cargo test -p unlucky-design --test card_neutral -- --nocapture`。
|
||||
///
|
||||
/// 格式 `CARD|key|flavor|before|role=hex;role=hex;…`(承接那张「8 张卡 surface 色块并排 +
|
||||
/// 切 sky→teal 窗口底色」的验收图;渲染脚本 `/d/tmp/m88/render.py` 只做排版,数据全在这里)。
|
||||
#[test]
|
||||
fn dump_card_palette_for_the_color_card() {
|
||||
const SHOWN: [Role; 10] = [
|
||||
Role::Surface,
|
||||
Role::OnSurface,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::SurfaceVariant,
|
||||
Role::SurfaceContainer,
|
||||
Role::SurfaceContainerHigh,
|
||||
Role::Outline,
|
||||
Role::Primary,
|
||||
Role::OnPrimary,
|
||||
Role::PrimaryContainer,
|
||||
];
|
||||
for card in CARDS {
|
||||
for flavor in flavors() {
|
||||
for (tag, scheme) in [
|
||||
("before", before_d43(card, flavor)),
|
||||
("after", card.scheme(flavor)),
|
||||
] {
|
||||
let pairs: Vec<String> = SHOWN
|
||||
.iter()
|
||||
.map(|role| format!("{}={}", role.name(), scheme.get(*role).to_hex()))
|
||||
.collect();
|
||||
println!(
|
||||
"CARD|{}|{}|{tag}|{}",
|
||||
card.key,
|
||||
flavor.name(),
|
||||
pairs.join(";")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! `card_neutral` 测试组共用的帮手:改前路径的复原、模式列表、色相环距离。
|
||||
//!
|
||||
//! 拆成子模块是为了单文件 ≤400 行的纪律(`card_neutral.rs` 是 crate 根,`common` / `dump`
|
||||
//! 只是它的子模块;cargo 只把 `tests/card_neutral.rs` 当一个 test target)。
|
||||
|
||||
use unlucky_design::color::{Palettes, Scheme, SchemeFlavor};
|
||||
use unlucky_design::palette::Card;
|
||||
|
||||
/// 改前的路径(M87 及之前):中性调色板跟着种子染色(官方 TonalSpot)。
|
||||
///
|
||||
/// 产品代码已经不用它了,但公开 API 仍在(取色路径就靠它逐色染色),所以这里能原样复现「改前」。
|
||||
pub fn before_d43(card: Card, flavor: SchemeFlavor) -> Scheme {
|
||||
Scheme::from_palettes(
|
||||
&Palettes::from_seed_with_neutral_chroma(card.seed, card.neutral_chroma),
|
||||
flavor,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn flavors() -> [SchemeFlavor; 2] {
|
||||
[SchemeFlavor::Light, SchemeFlavor::Dark]
|
||||
}
|
||||
|
||||
/// 色相环上的最短距离(度)。
|
||||
pub fn hue_gap(a: f64, b: f64) -> f64 {
|
||||
let raw = (a - b).abs();
|
||||
raw.min(360.0 - raw)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! 出图脚本读的 hex 行(无断言,纯打印)+ D43 的实测表(本文件就是那张表的来源)。
|
||||
//!
|
||||
//! 跑法:`cargo test -p unlucky-design --test card_neutral -- --nocapture`
|
||||
//!
|
||||
//! ## 改前 / 改后 `surface` 实测(2026-11)
|
||||
//!
|
||||
//! | 卡 | 改前 `surface`(浅) | 改后 `surface`(浅) | 改前 HCT(浅) | 改后 HCT(浅) |
|
||||
//! |---|---|---|---|---|
|
||||
//! | sky | `#F6FAFF` | `#F6FAFF` | 236.86° / 6.41 | 236.86° / 6.41 |
|
||||
//! | lavender | `#FDF7FF` | `#F6FAFF` | 298.10° / 5.87 | 236.86° / 6.41 |
|
||||
//! | forest | `#EEFDEE` | `#F6FAFF` | 157.01° / 11.77 | 236.86° / 6.41 |
|
||||
//! | amber | `#FFF8F4` | `#F6FAFF` | 71.48° / 1.89 | 236.86° / 6.41 |
|
||||
//! | rose | `#FFF8F7` | `#F6FAFF` | 22.09° / 1.29 | 236.86° / 6.41 |
|
||||
//! | teal | `#EBFEF8` | `#F6FAFF` | 182.42° / 12.03 | 236.86° / 6.41 |
|
||||
//! | dusk | `#FBF8FF` | `#F6FAFF` | 280.31° / 5.44 | 236.86° / 6.41 |
|
||||
//! | olive | `#F9FBE1` | `#F6FAFF` | 120.99° / 11.84 | 236.86° / 6.41 |
|
||||
//!
|
||||
//! 深色档同理:改前 `#09151C`~`#1F0F10`、彩度 11.63~12.45,改后八张卡统一 `#09151C`
|
||||
//! (235.70° / 11.70)。
|
||||
//!
|
||||
//! ## 这条用例为什么没有断言
|
||||
//!
|
||||
//! 它是**数据导出**(`CARD|` 行给 `/d/tmp/m88/render.py` 排版成验收图),断言在同目录的
|
||||
//! `card_neutral.rs` 里。放这里是为了让根文件留在 ≤400 行以内。
|
||||
|
||||
use super::common::{before_d43, flavors};
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::palette::CARDS;
|
||||
|
||||
/// 出图脚本读的 hex 行(改前 + 改后并排),跑法:
|
||||
/// `cargo test -p unlucky-design --test card_neutral -- --nocapture`。
|
||||
///
|
||||
/// 格式 `CARD|key|flavor|before|role=hex;role=hex;…`(承接那张「8 张卡 surface 色块并排 +
|
||||
/// 切 sky→teal 窗口底色」的验收图;渲染脚本 `/d/tmp/m88/render.py` 只做排版,数据全在这里)。
|
||||
#[test]
|
||||
fn dump_card_palette_for_the_color_card() {
|
||||
const SHOWN: [Role; 10] = [
|
||||
Role::Surface,
|
||||
Role::OnSurface,
|
||||
Role::OnSurfaceVariant,
|
||||
Role::SurfaceVariant,
|
||||
Role::SurfaceContainer,
|
||||
Role::SurfaceContainerHigh,
|
||||
Role::Outline,
|
||||
Role::Primary,
|
||||
Role::OnPrimary,
|
||||
Role::PrimaryContainer,
|
||||
];
|
||||
for card in CARDS {
|
||||
for flavor in flavors() {
|
||||
for (tag, scheme) in [
|
||||
("before", before_d43(card, flavor)),
|
||||
("after", card.scheme(flavor)),
|
||||
] {
|
||||
let pairs: Vec<String> = SHOWN
|
||||
.iter()
|
||||
.map(|role| format!("{}={}", role.name(), scheme.get(*role).to_hex()))
|
||||
.collect();
|
||||
println!(
|
||||
"CARD|{}|{}|{tag}|{}",
|
||||
card.key,
|
||||
flavor.name(),
|
||||
pairs.join(";")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user