microsoft-webui-press 3.0.5

High-performance Static Site Generator powered by WebUI Framework
Documentation
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

use super::*;

type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;

fn config(appearance: &str) -> std::result::Result<DocsConfig, serde_json::Error> {
    let mut value: Value = serde_json::from_str(
        r#"{
        "site": {"title": "Appearance"},
        "basePath": "/docs/",
        "contentDir": ".",
        "nav": [],
        "sidebar": []
    }"#,
    )?;
    value["appearance"] = serde_json::from_str(appearance)?;
    serde_json::from_value(value)
}

#[test]
fn metadata_is_normalized_safe_and_shared_with_ssr() -> TestResult {
    let config = config(
        r#"{
        "defaultTheme": "default",
        "defaultMode": "dark",
        "themes": [
            {"id": "default", "label": "</script><script>bad</script>"},
            {"id": "compact", "label": "Compact"}
        ],
        "storage": {"theme": "mai-design-theme", "mode": "theme"}
    }"#,
    )?;
    let appearance =
        PreparedAppearance::prepare(&config, Path::new("."), &mut StateDependencies::default())?;
    assert!(appearance.has_multiple_themes());
    let state = appearance.state(ShowMode::All);
    assert_eq!(state["theme"], "default");
    assert_eq!(state["mode"], "dark");
    assert_eq!(state["resolvedMode"], "dark");
    assert_eq!(state["themes"][1]["id"], "compact");
    assert!(state.get("tokens").is_none());
    let head = appearance.head_tags(ShowMode::All, "/docs/appearance-init.js")?;
    let metadata = head.split("</script>").next().ok_or("missing JSON")?;
    assert!(metadata.contains("\\u003c/script\\u003e"));
    assert!(metadata.contains("\"theme\":\"mai-design-theme\""));
    assert!(!metadata.contains("<script>bad"));
    assert!(head.contains("<script src=\"/docs/appearance-init.js\"></script>"));
    assert!(!head.contains(BOOTSTRAP_SCRIPT));
    assert!(appearance
        .head_tags(ShowMode::Content, "/docs/appearance-init.js")?
        .is_empty());
    assert!(appearance
        .head_tags(ShowMode::All, "/docs/\"init.js")?
        .contains("/docs/&quot;init.js"));
    assert_eq!(appearance.state(ShowMode::Content)["mode"], "system");
    Ok(())
}

#[test]
fn invalid_configuration_is_actionable() -> TestResult {
    for appearance in [
        r#"{"defaultTheme":"unknown", "themes":[{"id":"a","label":"A"}]}"#,
        r#"{"defaultTheme":"a", "themes":[{"id":"a","label":"A"},{"id":"a","label":"Duplicate"}]}"#,
        r#"{"defaultTheme":"x\"]", "themes":[{"id":"x\"]","label":"Unsafe"}]}"#,
        r#"{"defaultTheme":"a", "themes":[{"id":"a","label":" "}]}"#,
        r#"{"defaultTheme":"a", "themes":[{"id":"a","label":"A"}], "storage":{"theme":"same","mode":"same"}}"#,
    ] {
        let config = config(appearance)?;
        let error =
            PreparedAppearance::prepare(&config, Path::new("."), &mut StateDependencies::default())
                .err()
                .ok_or("expected configuration error")?;
        assert!(error.to_string().contains("help:"));
    }
    let mut config = config(r#"{"defaultTheme":"a", "themes":[{"id":"a","label":"A"}]}"#)?;
    config.theme = Some("./tokens.json".into());
    assert!(PreparedAppearance::prepare(
        &config,
        Path::new("."),
        &mut StateDependencies::default()
    )
    .is_err());
    Ok(())
}

#[test]
fn snapshots_preserve_literals_resets_and_external_input_versions() -> TestResult {
    let dir = tempfile::tempdir()?;
    let source = dir.path().join("family.json");
    fs::write(
        &source,
        r##"{"themes":{"light":{"surface":"#fff","spacing":"initial","alias":"var(--surface)"},"dark":{"surface":"#000","spacing":"12px"}}}"##,
    )?;
    let mut config = config(
        r#"{
        "defaultTheme": "official",
        "themes": [
            {"id":"official", "label":"Official"},
            {"id":"css", "label":"CSS only"}
        ]
    }"#,
    )?;
    config
        .appearance
        .as_mut()
        .ok_or("missing appearance")?
        .themes[0]
        .source = Some(source.to_string_lossy().into_owned());
    let mut dependencies = StateDependencies::default();
    let appearance = PreparedAppearance::prepare(&config, Path::new("."), &mut dependencies)?;
    assert!(appearance.css.contains("--spacing: initial;"));
    assert!(appearance.css.contains("--spacing: 12px;"));
    assert!(appearance.css.contains("--alias: var(--surface);"));
    assert!(appearance.css.contains("--alias: initial;"));
    assert!(appearance
        .css
        .contains(":root:where([data-press-theme=\"css\"])"));
    assert!(appearance.css.contains("prefers-color-scheme: dark"));
    assert_eq!(dependencies.paths, vec![source.canonicalize()?]);
    assert!(dependencies.changed_paths().is_empty());
    let again =
        PreparedAppearance::prepare(&config, Path::new("."), &mut StateDependencies::default())?;
    assert_eq!(appearance.css, again.css);
    let validation = appearance.validation_tokens().ok_or("missing validation")?;
    assert!(validation
        .themes
        .values()
        .all(|tokens| tokens.values().all(String::is_empty)));
    assert!(webui_tokens::validate_required_tokens(&["surface".into()], &validation).is_ok());
    assert!(webui_tokens::validate_required_tokens(&["alias".into()], &validation).is_err());
    fs::write(&source, r#"{"themes":{"light":{}}}"#)?;
    assert_eq!(dependencies.changed_paths(), vec![source.canonicalize()?]);
    let error =
        PreparedAppearance::prepare(&config, Path::new("."), &mut StateDependencies::default())
            .err()
            .ok_or("missing scheme accepted")?;
    assert!(error.to_string().contains("missing its 'dark' map"));
    Ok(())
}

#[test]
fn legacy_and_content_contracts_remain_separate() -> TestResult {
    let dir = tempfile::tempdir()?;
    fs::write(
        dir.path().join("tokens.json"),
        r##"{"themes":{"light":{"color":"#fff"},"dark":{"color":"#000"}}}"##,
    )?;
    let mut config = config("null")?;
    config.theme = Some("./tokens.json".into());
    let legacy =
        PreparedAppearance::prepare(&config, dir.path(), &mut StateDependencies::default())?;
    assert!(legacy.legacy_tokens().is_some());
    assert!(!legacy.has_multiple_themes());
    assert_eq!(legacy.metadata.storage.mode, "webui-press:/docs/:mode");
    config.theme = None;
    config.appearance = serde_json::from_str(
        r#"{
        "defaultTheme":"official", "defaultMode":"dark",
        "themes":[{"id":"official","label":"Official","source":"./tokens.json"},{"id":"css","label":"CSS"}]
    }"#,
    )?;
    config.show = ShowMode::Content;
    let content =
        PreparedAppearance::prepare(&config, dir.path(), &mut StateDependencies::default())?;
    assert!(!content.css.contains("data-theme"));
    assert!(!content.css.contains("data-press-theme"));
    assert!(content.css.contains("--color: #000;"));
    assert!(content
        .head_tags(config.show, "/docs/appearance-init.js")?
        .is_empty());
    Ok(())
}

#[test]
fn build_emits_shared_artifacts_and_metadata_for_pages_and_404() -> TestResult {
    let dir = tempfile::tempdir()?;
    let content = dir.path().join("content");
    let template = dir.path().join("template");
    fs::create_dir_all(&content)?;
    fs::create_dir_all(&template)?;
    fs::write(
        content.join("index.md"),
        "# Appearance\n\nA complete build.",
    )?;
    fs::write(
        template.join("index.html"),
        r#"<!doctype html><html data-press-theme="{{webuiPress.appearance.theme}}" data-theme-mode="{{webuiPress.appearance.mode}}"><head>{{{headTags}}}</head><body><h1>{{page.title}}</h1>{{{page.content}}}<span>{{webuiPress.appearance.resolvedMode}}</span><output>{{appearance}}</output></body></html>"#,
    )?;
    fs::write(
        dir.path().join("tokens.json"),
        r##"{"themes":{"light":{"surface":"#fff"},"dark":{"surface":"#000"}}}"##,
    )?;
    fs::write(dir.path().join("custom.css"), ":root { --surface: pink; }")?;
    let mut config = config(
        r#"{
        "defaultTheme":"official", "defaultMode":"dark",
        "themes":[{"id":"official","label":"Official","source":"./tokens.json"}]
    }"#,
    )?;
    config.content_dir = content.to_string_lossy().into_owned();
    config.out_dir = dir.path().join("output").to_string_lossy().into_owned();
    config.public_dir = dir.path().join("no-public").to_string_lossy().into_owned();
    config.css = Some(dir.path().join("custom.css").to_string_lossy().into_owned());
    config.state = Some(serde_json::from_str(r#"{"appearance":"subtle"}"#)?);
    crate::build::build_docs(&config, dir.path(), &template)?;
    let output = Path::new(&config.out_dir);
    let mut bootstrap_file = None;
    for entry in fs::read_dir(output)? {
        let path = entry?.path();
        if path
            .file_name()
            .and_then(|name| name.to_str())
            .is_some_and(|name| name.starts_with("appearance-init-") && name.ends_with(".js"))
        {
            assert!(bootstrap_file.is_none(), "bootstrap must be shared");
            bootstrap_file = Some(path);
        }
    }
    let bootstrap_file = bootstrap_file.ok_or("missing bootstrap asset")?;
    assert_eq!(fs::read_to_string(&bootstrap_file)?, BOOTSTRAP_SCRIPT);
    let bootstrap_tag = format!(
        "<script src=\"/docs/{}\"></script>",
        bootstrap_file
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or("invalid asset name")?
    );
    for path in ["index.html", "404.html"] {
        let html = fs::read_to_string(output.join(path))?;
        assert!(html.contains("data-press-theme=\"official\""));
        assert!(html.contains("data-theme-mode=\"dark\""));
        assert!(html.contains("<span>dark</span>"));
        assert!(html.contains("<output>subtle</output>"));
        let bootstrap = html
            .find("id=\"webui-press-appearance\"")
            .ok_or("missing bootstrap")?;
        let script = html
            .find(&bootstrap_tag)
            .ok_or("missing bootstrap script")?;
        let appearance = html
            .find("/docs/appearance.css")
            .ok_or("missing appearance CSS")?;
        let custom = html.find("/docs/theme.css").ok_or("missing consumer CSS")?;
        assert!(bootstrap < script && script < appearance && appearance < custom);
        assert!(!html.contains(BOOTSTRAP_SCRIPT));
        assert!(!html.contains("#fff"));
    }

    assert!(fs::read_to_string(output.join("appearance.css"))?.contains("--surface: #000;"));
    config.show = ShowMode::Content;
    crate::build::build_docs(&config, dir.path(), &template)?;
    assert!(!bootstrap_file.exists());
    for path in ["index.html", "404.html"] {
        let html = fs::read_to_string(output.join(path))?;
        assert!(!html.contains("id=\"webui-press-appearance\""));
        assert!(!html.contains("localStorage"));
        assert!(html.contains("/docs/appearance.css"));
    }
    config.state = Some(serde_json::from_str(
        r#"{"webuiPress":{"appearance":"conflict"}}"#,
    )?);
    let error = crate::build::build_docs(&config, dir.path(), &template)
        .err()
        .ok_or("reserved namespace accepted")?
        .to_string();
    assert!(
        error.contains("webuiPress") && error.contains("reserved"),
        "{error}"
    );
    Ok(())
}

#[test]
fn sourced_families_use_external_css_validation_context() -> TestResult {
    let dir = tempfile::tempdir()?;
    fs::write(
        dir.path().join("tokens.json"),
        r##"{"themes":{"light":{"surface":"#fff"},"dark":{"surface":"#000"}}}"##,
    )?;
    let config = config(
        r#"{"defaultTheme":"official","themes":[{"id":"official","label":"Official","source":"./tokens.json"}]}"#,
    )?;
    let mut appearance =
        PreparedAppearance::prepare(&config, dir.path(), &mut StateDependencies::default())?;
    let required = ["docs-color-bg-alt".into(), "consumer-surface".into()];
    let before = appearance.validation_tokens().ok_or("missing validation")?;
    assert!(webui_tokens::validate_required_tokens(&required, &before).is_err());
    appearance.add_css_validation_context(include_str!("../template/docs.css"))?;
    appearance.add_css_validation_context(":root { --consumer-surface: var(--surface); }")?;
    let validation = appearance.validation_tokens().ok_or("missing validation")?;
    webui_tokens::validate_required_tokens(&required, &validation)?;
    assert!(
        webui_tokens::validate_required_tokens(&["consumer-surfac".into()], &validation).is_err()
    );
    assert!(!appearance.css.contains("--docs-color-bg-alt:"));
    assert!(!appearance.css.contains("--consumer-surface:"));
    assert!(appearance.css.contains("--surface: #fff;"));
    Ok(())
}