test(api/markdown): 解耦 wrap_images_with_blur 并脱离文件系统依赖
把 wrap_images_with_blur 拆成两部分: - wrap_images_with_blur_with(html, dims_fn):纯函数核心,接受 dimensions 查询闭包,不碰文件系统 - wrap_images_with_blur(html):薄包装,注入真实 get_image_dimensions 测试改用纯函数核心,注入确定性 dimensions(如 Some((800,600)) 或 None), 彻底脱离 uploads/ 真实文件依赖。此前的 aspect-ratio 测试硬编码了 /uploads/2026/06/18/...webp 真实路径——文件不存在则静默走缺省分支, 断言虽能过但实际验证内容被削弱。 新增覆盖(共 8 个 wrap 相关测试): - --ar 缺省/有值两种路径 - rel_path 去查询的验证(用 RefCell 捕获 Fn 闭包入参) - alt 保留(双层)/空 alt 不生成属性 - 斜杠分隔的 --ar 格式 + sanitizer 管线不破坏斜杠
This commit is contained in:
parent
ab05c01f76
commit
edee09596c
@ -209,6 +209,18 @@ pub fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
|||||||
/// 3. 生成 `<span class="blur-img" style="--ar:..">` 包裹两层 img
|
/// 3. 生成 `<span class="blur-img" style="--ar:..">` 包裹两层 img
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
fn wrap_images_with_blur(html: &str) -> String {
|
fn wrap_images_with_blur(html: &str) -> String {
|
||||||
|
wrap_images_with_blur_with(html, crate::api::image::get_image_dimensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `wrap_images_with_blur` 的纯函数核心,接受 dimensions 查询闭包以便单测。
|
||||||
|
///
|
||||||
|
/// `dims_fn(rel_path) -> Option<(u32, u32)>` 注入真实或测试用的 dimensions 来源,
|
||||||
|
/// 使本函数不依赖文件系统——测试可注入已知宽高,生产注入 get_image_dimensions。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
fn wrap_images_with_blur_with<F>(html: &str, dims_fn: F) -> String
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Option<(u32, u32)>,
|
||||||
|
{
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
@ -233,7 +245,7 @@ fn wrap_images_with_blur(html: &str) -> String {
|
|||||||
|
|
||||||
// 查 dimensions,算 aspect-ratio
|
// 查 dimensions,算 aspect-ratio
|
||||||
// 注意:CSS aspect-ratio 用斜杠分隔(width / height),不是冒号
|
// 注意:CSS aspect-ratio 用斜杠分隔(width / height),不是冒号
|
||||||
let ar_style = crate::api::image::get_image_dimensions(rel_path)
|
let ar_style = dims_fn(rel_path)
|
||||||
.map(|(w, h)| format!(" style=\"--ar:{} / {};\"", w, h))
|
.map(|(w, h)| format!(" style=\"--ar:{} / {};\"", w, h))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@ -345,10 +357,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wrap_images_with_blur_wraps_uploads_image() {
|
fn wrap_images_with_blur_wraps_uploads_image() {
|
||||||
// 注意:此测试依赖 uploads/ 目录下存在对应文件才能拿到 dimensions。
|
// 注入返回 None 的 dims_fn,验证 --ar 缺省时的结构正确性。
|
||||||
// 用一个不含 dimensions 的路径验证 --ar 缺省时的结构正确性。
|
// 不依赖 uploads/ 文件系统。
|
||||||
let html = r#"<p><img src="/uploads/nonexistent/test.webp" alt="test"></p>"#;
|
let html = r#"<p><img src="/uploads/nonexistent/test.webp" alt="test"></p>"#;
|
||||||
let result = wrap_images_with_blur(html);
|
let result = wrap_images_with_blur_with(html, |_| None);
|
||||||
assert!(
|
assert!(
|
||||||
result.contains("blur-img-placeholder"),
|
result.contains("blur-img-placeholder"),
|
||||||
"should have placeholder"
|
"should have placeholder"
|
||||||
@ -361,9 +373,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wrap_images_with_blur_skips_external_image() {
|
fn wrap_images_with_blur_skips_external_image() {
|
||||||
|
// 外链图不进入 dims_fn,保持原样。
|
||||||
let html = r#"<img src="https://example.com/img.png" alt="ext">"#;
|
let html = r#"<img src="https://example.com/img.png" alt="ext">"#;
|
||||||
let result = wrap_images_with_blur(html);
|
let result = wrap_images_with_blur_with(html, |_| None);
|
||||||
// 外链图不处理,保持原样
|
|
||||||
assert!(
|
assert!(
|
||||||
!result.contains("blur-img"),
|
!result.contains("blur-img"),
|
||||||
"external image should not be wrapped"
|
"external image should not be wrapped"
|
||||||
@ -372,28 +384,71 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wrap_images_with_blur_uses_slash_in_aspect_ratio() {
|
fn wrap_images_with_blur_uses_slash_in_aspect_ratio() {
|
||||||
// 用真实存在的 uploads 文件,让 dimensions 成功返回,验证 --ar 格式
|
// 注入已知 dimensions,验证 --ar 用斜杠分隔(如 "--ar:800 / 600;")。
|
||||||
let html = r#"<img src="/uploads/2026/06/18/090402.21c4f0b6-6d5a-49ee-bc79-99cb557ff385.webp" alt="t">"#;
|
// 此前依赖 uploads/ 真实文件,现已解耦。
|
||||||
let result = wrap_images_with_blur(html);
|
let html = r#"<img src="/uploads/2026/06/18/abc.webp" alt="t">"#;
|
||||||
println!("ACTUAL OUTPUT: {}", result);
|
let result = wrap_images_with_blur_with(html, |_| Some((800, 600)));
|
||||||
// aspect-ratio 必须用斜杠分隔,如 "--ar:800 / 600;"
|
|
||||||
assert!(result.contains("--ar:"), "should have --ar");
|
assert!(result.contains("--ar:"), "should have --ar");
|
||||||
assert!(
|
assert!(
|
||||||
result.contains(" / "),
|
result.contains(" / "),
|
||||||
"aspect-ratio must use slash separator, got: {}",
|
"aspect-ratio must use slash separator, got: {}",
|
||||||
result
|
result
|
||||||
);
|
);
|
||||||
|
assert!(result.contains("--ar:800 / 600;"), "应含精确宽高");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrap_images_with_blur_omits_ar_when_no_dimensions() {
|
||||||
|
// dims_fn 返回 None 时不输出 --ar,避免空 style 属性。
|
||||||
|
let html = r#"<img src="/uploads/x.webp" alt="t">"#;
|
||||||
|
let result = wrap_images_with_blur_with(html, |_| None);
|
||||||
|
assert!(!result.contains("--ar"), "无 dimensions 不应有 --ar");
|
||||||
|
// 但包裹结构仍应生成
|
||||||
|
assert!(result.contains("blur-img"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrap_images_with_blur_strips_query_from_rel_path() {
|
||||||
|
// rel_path 提取应去 query 后缀,dims_fn 收到的是去 query 的路径。
|
||||||
|
// dims_fn 是 Fn(replace_all 要求),用 RefCell 捕获传入值。
|
||||||
|
use std::cell::RefCell;
|
||||||
|
let html = r#"<img src="/uploads/2026/x.webp?w=100" alt="t">"#;
|
||||||
|
let received = RefCell::new(String::new());
|
||||||
|
let _ = wrap_images_with_blur_with(html, |p| {
|
||||||
|
*received.borrow_mut() = p.to_string();
|
||||||
|
Some((100, 100))
|
||||||
|
});
|
||||||
|
assert_eq!(received.borrow().as_str(), "2026/x.webp", "rel_path 应去 query");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrap_images_with_blur_preserves_alt() {
|
||||||
|
let html = r#"<img src="/uploads/x.webp" alt="描述">"#;
|
||||||
|
let result = wrap_images_with_blur_with(html, |_| Some((10, 10)));
|
||||||
|
assert!(result.contains(r#"alt="描述""#), "placeholder 应保留 alt");
|
||||||
|
assert!(
|
||||||
|
result.matches(r#"alt="描述""#).count() == 2,
|
||||||
|
"两层 img 都应带 alt"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrap_images_with_blur_omits_alt_attr_when_empty() {
|
||||||
|
let html = r#"<img src="/uploads/x.webp">"#;
|
||||||
|
let result = wrap_images_with_blur_with(html, |_| None);
|
||||||
|
assert!(
|
||||||
|
!result.contains("alt=\""),
|
||||||
|
"无 alt 时不应生成空 alt 属性"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn full_pipeline_wrap_then_clean_preserves_slash() {
|
fn full_pipeline_wrap_then_clean_preserves_slash() {
|
||||||
// 模拟完整渲染管线:wrap_images_with_blur → clean_html
|
// 模拟完整渲染管线:wrap → clean_html,验证 sanitizer 不破坏斜杠。
|
||||||
// 验证 sanitizer 不把斜杠转义或删掉
|
// 注入确定 dimensions,脱离文件系统依赖。
|
||||||
let html = r#"<img src="/uploads/2026/06/18/090402.21c4f0b6-6d5a-49ee-bc79-99cb557ff385.webp" alt="t">"#;
|
let html = r#"<img src="/uploads/2026/06/18/abc.webp" alt="t">"#;
|
||||||
let wrapped = wrap_images_with_blur(html);
|
let wrapped = wrap_images_with_blur_with(html, |_| Some((800, 600)));
|
||||||
let cleaned = clean_html(&wrapped);
|
let cleaned = clean_html(&wrapped);
|
||||||
println!("WRAPPED: {}", wrapped);
|
|
||||||
println!("CLEANED: {}", cleaned);
|
|
||||||
assert!(
|
assert!(
|
||||||
cleaned.contains(" / "),
|
cleaned.contains(" / "),
|
||||||
"clean_html must preserve slash in --ar, got: {}",
|
"clean_html must preserve slash in --ar, got: {}",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user