webpage_quality_analyzer 1.0.2

High-performance webpage quality analyzer with 115 comprehensive metrics - Rust library with WASM, C++, and Python bindings
Documentation
use webpage_quality_analyzer::{
    analyze, analyze_with_profile,
    models::models::{AnalyzeError, QualityBand},
};

const GOOD_HTML: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Quality Web Page</title>
    <meta name="description" content="A well-structured web page with good content and proper SEO.">
</head>
<body>
    <h1>Main Heading</h1>
    <p>This is a well-written article with multiple paragraphs and good structure.</p>
    <h2>Section One</h2>
    <p>Content continues here with meaningful text that provides value to readers.</p>
    <h3>Subsection</h3>
    <ul>
        <li>First item</li>
        <li>Second item</li>
    </ul>
    <p>More content with <a href="https://example.com">external links</a> and proper structure.</p>
</body>
</html>"#;

const MINIMAL_HTML: &str = r#"<!DOCTYPE html>
<html>
<head><title>Simple</title></head>
<body><h1>Hello</h1><p>Basic content.</p></body>
</html>"#;

#[tokio::test]
async fn test_basic_analysis() {
    let report = analyze("https://example.com/test", Some(GOOD_HTML))
        .await
        .expect("Analysis failed");

    // Basic validations
    assert_eq!(report.url, "https://example.com/test");
    assert!(report.score >= 0.0 && report.score <= 100.0);
    assert!(!report.version.is_empty());

    // Quality band should be valid
    assert!(matches!(
        report.verdict,
        QualityBand::VeryPoor
            | QualityBand::Poor
            | QualityBand::Fair
            | QualityBand::Good
            | QualityBand::Excellent
    ));
}

#[tokio::test]
async fn test_content_metrics() {
    let report = analyze("https://example.com/test", Some(GOOD_HTML))
        .await
        .unwrap();

    let html_metrics = &report.metrics.html_analysis;

    // Should have content
    assert!(html_metrics.content.word_count > 10);
    assert!(html_metrics.structure.paragraph_count > 0);
    assert!(html_metrics.structure.headings_count > 0);

    // Should have reasonable values
    assert!(html_metrics.seo.title_len > 0);
    assert!(html_metrics.content.reading_time_minutes >= 0.0);
    assert!(html_metrics.content.content_to_html_ratio > 0.0);
}

#[tokio::test]
async fn test_metadata_extraction() {
    let report = analyze("https://example.com/test", Some(GOOD_HTML))
        .await
        .unwrap();

    assert_eq!(report.metadata.title, "Quality Web Page");
    assert!(report.metadata.meta_description.is_some());
    assert!(report
        .metadata
        .meta_description
        .as_ref()
        .unwrap()
        .contains("well-structured"));
}

#[tokio::test]
async fn test_html_metrics() {
    let report = analyze("https://example.com/test", Some(GOOD_HTML))
        .await
        .unwrap();

    let html_metrics = &report.metrics.html_analysis;

    // Should have content
    assert!(html_metrics.content.word_count > 10);
    assert!(html_metrics.structure.paragraph_count > 0);
    assert!(html_metrics.structure.headings_count > 0);

    // Should have reasonable values
    assert!(html_metrics.seo.title_len > 0);
    assert!(html_metrics.content.reading_time_minutes >= 0.0);
    assert!(html_metrics.content.content_to_html_ratio > 0.0);
}

#[tokio::test]
async fn test_minimal_content() {
    let report = analyze("https://example.com/minimal", Some(MINIMAL_HTML))
        .await
        .unwrap();

    assert!(report.score >= 0.0 && report.score <= 100.0);
    assert_eq!(report.metadata.title, "Simple");

    let html_metrics = &report.metrics.html_analysis;
    assert!(html_metrics.content.word_count > 0);
    assert!(html_metrics.structure.headings_count > 0);
}

#[tokio::test]
async fn test_different_profiles() {
    let url = "https://example.com/test";

    // Test with article profile
    let report1 = analyze_with_profile(url, Some(GOOD_HTML), "content_article")
        .await
        .unwrap();

    // Test with news profile
    let report2 = analyze_with_profile(url, Some(GOOD_HTML), "news")
        .await
        .unwrap();

    // Both should produce valid scores
    assert!(report1.score >= 0.0 && report1.score <= 100.0);
    assert!(report2.score >= 0.0 && report2.score <= 100.0);
}

#[tokio::test]
async fn test_error_handling() {
    // Test empty URL
    let result = analyze("", Some(GOOD_HTML)).await;
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), AnalyzeError::InvalidUrl(_)));

    // Test invalid profile
    let result =
        analyze_with_profile("https://example.com", Some(GOOD_HTML), "invalid_profile").await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_processed_document() {
    let report = analyze("https://example.com/test", Some(GOOD_HTML))
        .await
        .unwrap();

    // Should extract headings
    assert!(!report.processed_document.headings.is_empty());

    // Should have main content
    assert!(!report.processed_document.cleaned_text.is_empty());

    // Should extract links if present
    if !report.processed_document.links.is_empty() {
        // Use resolved_href for absolute URL check
        assert!(report.processed_document.links[0].resolved_href.len() > 0);
        // Optional: check raw_href as well
        assert!(report.processed_document.links[0].raw_href.len() > 0);
    }
}

#[tokio::test]
async fn test_quality_bands() {
    // Test good content
    let good_report = analyze("https://example.com/good", Some(GOOD_HTML))
        .await
        .unwrap();

    // Test minimal content
    let minimal_report = analyze("https://example.com/minimal", Some(MINIMAL_HTML))
        .await
        .unwrap();

    // Debug output for understanding scores
    println!(
        "Good HTML score: {}, verdict: {:?}",
        good_report.score, good_report.verdict
    );
    println!(
        "Minimal HTML score: {}, verdict: {:?}",
        minimal_report.score, minimal_report.verdict
    );

    // Ensure scores are always in valid range
    assert!(good_report.score >= 0.0 && good_report.score <= 100.0);
    assert!(minimal_report.score >= 0.0 && minimal_report.score <= 100.0);

    // Verify that the quality bands are correctly assigned based on actual thresholds:
    // Excellent: 90.0+, Good: 70.0+, Fair: 50.0+, Poor: 30.0+, VeryPoor: 0.0-29.9
    if good_report.score >= 90.0 {
        assert!(matches!(good_report.verdict, QualityBand::Excellent));
    } else if good_report.score >= 70.0 {
        assert!(matches!(good_report.verdict, QualityBand::Good));
    } else if good_report.score >= 50.0 {
        assert!(matches!(good_report.verdict, QualityBand::Fair));
    } else if good_report.score >= 30.0 {
        assert!(matches!(good_report.verdict, QualityBand::Poor));
    } else {
        assert!(matches!(good_report.verdict, QualityBand::VeryPoor));
    }

    if minimal_report.score >= 90.0 {
        assert!(matches!(minimal_report.verdict, QualityBand::Excellent));
    } else if minimal_report.score >= 70.0 {
        assert!(matches!(minimal_report.verdict, QualityBand::Good));
    } else if minimal_report.score >= 50.0 {
        assert!(matches!(minimal_report.verdict, QualityBand::Fair));
    } else if minimal_report.score >= 30.0 {
        assert!(matches!(minimal_report.verdict, QualityBand::Poor));
    } else {
        assert!(matches!(minimal_report.verdict, QualityBand::VeryPoor));
    }
}