mod css_builder;
mod html_builder;
mod js_builder;
mod validator;
pub use css_builder::{CssBuilder, CssRule, GeneratedCss};
pub use html_builder::{Element, GeneratedHtml, HtmlBuilder, HtmlDocument};
pub use js_builder::{GeneratedJs, JsBuilder, WasmConfig};
pub use validator::{
AccessibilityIssue as WebAccessibilityIssue, CssLintResult, HtmlValidationResult, JsLintResult,
SecurityIssue, Severity as WebSeverity, ValidationReport, WebValidator,
};
#[derive(Debug, Clone)]
pub struct WebBundle {
pub html: GeneratedHtml,
pub css: GeneratedCss,
pub js: GeneratedJs,
pub validation: ValidationReport,
}
impl WebBundle {
#[must_use]
pub fn new(html: GeneratedHtml, css: GeneratedCss, js: GeneratedJs) -> Self {
let validation = WebValidator::validate_all(&html, &css, &js);
Self {
html,
css,
js,
validation,
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.validation.is_valid()
}
#[must_use]
pub fn to_single_file(&self) -> String {
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
{css}
</style>
</head>
<body>
{body}
<script>
{js}
</script>
</body>
</html>"#,
title = self.html.title,
css = self.css.content,
body = self.html.body_content,
js = self.js.content,
)
}
}
#[derive(Debug, Clone, Default)]
pub struct WebAssetCoverage {
pub html_elements: std::collections::HashMap<String, WebElementCoverage>,
pub css_rules: std::collections::HashMap<String, RuleCoverage>,
pub js_functions: std::collections::HashMap<String, FunctionCoverage>,
}
#[derive(Debug, Clone, Default)]
pub struct WebElementCoverage {
pub id: String,
pub interaction_count: u64,
pub interaction_types: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct RuleCoverage {
pub selector: String,
pub application_count: u64,
}
#[derive(Debug, Clone, Default)]
pub struct FunctionCoverage {
pub name: String,
pub execution_count: u64,
}
impl WebAssetCoverage {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn element_used(&mut self, id: &str, interaction_type: &str) {
let entry = self.html_elements.entry(id.to_string()).or_default();
entry.id = id.to_string();
entry.interaction_count += 1;
if !entry
.interaction_types
.contains(&interaction_type.to_string())
{
entry.interaction_types.push(interaction_type.to_string());
}
}
pub fn rule_applied(&mut self, selector: &str) {
let entry = self.css_rules.entry(selector.to_string()).or_default();
entry.selector = selector.to_string();
entry.application_count += 1;
}
pub fn function_executed(&mut self, name: &str) {
let entry = self.js_functions.entry(name.to_string()).or_default();
entry.name = name.to_string();
entry.execution_count += 1;
}
#[must_use]
pub fn coverage_percent(&self) -> f64 {
let html_covered = self
.html_elements
.values()
.filter(|e| e.interaction_count > 0)
.count();
let css_covered = self
.css_rules
.values()
.filter(|r| r.application_count > 0)
.count();
let js_covered = self
.js_functions
.values()
.filter(|f| f.execution_count > 0)
.count();
let total = self.html_elements.len() + self.css_rules.len() + self.js_functions.len();
let covered = html_covered + css_covered + js_covered;
if total == 0 {
100.0
} else {
(covered as f64 / total as f64) * 100.0
}
}
#[must_use]
pub fn report(&self) -> WebAssetCoverageReport {
WebAssetCoverageReport {
html_coverage: self.calculate_html_coverage(),
css_coverage: self.calculate_css_coverage(),
js_coverage: self.calculate_js_coverage(),
overall_coverage: self.coverage_percent(),
}
}
fn calculate_html_coverage(&self) -> f64 {
let total = self.html_elements.len();
let covered = self
.html_elements
.values()
.filter(|e| e.interaction_count > 0)
.count();
if total == 0 {
100.0
} else {
(covered as f64 / total as f64) * 100.0
}
}
fn calculate_css_coverage(&self) -> f64 {
let total = self.css_rules.len();
let covered = self
.css_rules
.values()
.filter(|r| r.application_count > 0)
.count();
if total == 0 {
100.0
} else {
(covered as f64 / total as f64) * 100.0
}
}
fn calculate_js_coverage(&self) -> f64 {
let total = self.js_functions.len();
let covered = self
.js_functions
.values()
.filter(|f| f.execution_count > 0)
.count();
if total == 0 {
100.0
} else {
(covered as f64 / total as f64) * 100.0
}
}
}
#[derive(Debug, Clone)]
pub struct WebAssetCoverageReport {
pub html_coverage: f64,
pub css_coverage: f64,
pub js_coverage: f64,
pub overall_coverage: f64,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn h0_web_01_bundle_creation() {
let html = HtmlBuilder::new()
.title("Test App")
.canvas("app", 800, 600)
.build()
.unwrap();
let css = CssBuilder::new().responsive_canvas("app").build().unwrap();
let js = JsBuilder::new("app.wasm", "app").build().unwrap();
let bundle = WebBundle::new(html, css, js);
assert!(bundle.is_valid());
}
#[test]
fn h0_web_02_bundle_single_file_output() {
let html = HtmlBuilder::new()
.title("Test")
.canvas("canvas", 100, 100)
.build()
.unwrap();
let css = CssBuilder::new().build().unwrap();
let js = JsBuilder::new("test.wasm", "canvas").build().unwrap();
let bundle = WebBundle::new(html, css, js);
let output = bundle.to_single_file();
assert!(output.contains("<!DOCTYPE html>"));
assert!(output.contains("<title>Test</title>"));
assert!(output.contains("<style>"));
assert!(output.contains("<script>"));
}
#[test]
fn h0_web_03_coverage_tracking() {
let mut coverage = WebAssetCoverage::new();
coverage.html_elements.insert(
"button".to_string(),
WebElementCoverage {
id: "button".to_string(),
interaction_count: 0,
interaction_types: vec![],
},
);
coverage.css_rules.insert(
"#button".to_string(),
RuleCoverage {
selector: "#button".to_string(),
application_count: 0,
},
);
assert_eq!(coverage.coverage_percent(), 0.0);
coverage.element_used("button", "click");
assert!(coverage.coverage_percent() > 0.0);
coverage.rule_applied("#button");
assert_eq!(coverage.coverage_percent(), 100.0);
}
#[test]
fn h0_web_04_coverage_report() {
let mut coverage = WebAssetCoverage::new();
coverage.html_elements.insert(
"a".to_string(),
WebElementCoverage {
id: "a".to_string(),
interaction_count: 1,
interaction_types: vec!["click".to_string()],
},
);
coverage.html_elements.insert(
"b".to_string(),
WebElementCoverage {
id: "b".to_string(),
interaction_count: 0,
interaction_types: vec![],
},
);
let report = coverage.report();
assert_eq!(report.html_coverage, 50.0);
assert_eq!(report.css_coverage, 100.0); assert_eq!(report.js_coverage, 100.0); }
#[test]
fn h0_web_05_empty_coverage_is_100_percent() {
let coverage = WebAssetCoverage::new();
assert_eq!(coverage.coverage_percent(), 100.0);
}
#[test]
fn h0_web_06_function_executed_tracking() {
let mut coverage = WebAssetCoverage::new();
coverage.function_executed("init");
assert!(coverage.js_functions.contains_key("init"));
let func = coverage.js_functions.get("init").unwrap();
assert_eq!(func.name, "init");
assert_eq!(func.execution_count, 1);
coverage.function_executed("init");
let func = coverage.js_functions.get("init").unwrap();
assert_eq!(func.execution_count, 2);
}
#[test]
fn h0_web_07_function_coverage_calculation() {
let mut coverage = WebAssetCoverage::new();
coverage.js_functions.insert(
"fn1".to_string(),
FunctionCoverage {
name: "fn1".to_string(),
execution_count: 5,
},
);
coverage.js_functions.insert(
"fn2".to_string(),
FunctionCoverage {
name: "fn2".to_string(),
execution_count: 0,
},
);
let report = coverage.report();
assert_eq!(report.js_coverage, 50.0);
}
#[test]
fn h0_web_08_element_used_duplicate_interaction_type() {
let mut coverage = WebAssetCoverage::new();
coverage.element_used("btn", "click");
coverage.element_used("btn", "click"); coverage.element_used("btn", "hover");
let elem = coverage.html_elements.get("btn").unwrap();
assert_eq!(elem.interaction_count, 3);
assert_eq!(elem.interaction_types.len(), 2);
assert!(elem.interaction_types.contains(&"click".to_string()));
assert!(elem.interaction_types.contains(&"hover".to_string()));
}
#[test]
fn h0_web_09_rule_applied_multiple_times() {
let mut coverage = WebAssetCoverage::new();
coverage.rule_applied(".container");
coverage.rule_applied(".container");
coverage.rule_applied(".container");
let rule = coverage.css_rules.get(".container").unwrap();
assert_eq!(rule.selector, ".container");
assert_eq!(rule.application_count, 3);
}
#[test]
fn h0_web_10_css_coverage_partial() {
let mut coverage = WebAssetCoverage::new();
coverage.css_rules.insert(
"#r1".to_string(),
RuleCoverage {
selector: "#r1".to_string(),
application_count: 1,
},
);
coverage.css_rules.insert(
"#r2".to_string(),
RuleCoverage {
selector: "#r2".to_string(),
application_count: 0,
},
);
coverage.css_rules.insert(
"#r3".to_string(),
RuleCoverage {
selector: "#r3".to_string(),
application_count: 0,
},
);
coverage.css_rules.insert(
"#r4".to_string(),
RuleCoverage {
selector: "#r4".to_string(),
application_count: 0,
},
);
let report = coverage.report();
assert_eq!(report.css_coverage, 25.0);
}
#[test]
fn h0_web_11_overall_coverage_mixed() {
let mut coverage = WebAssetCoverage::new();
coverage.html_elements.insert(
"e1".to_string(),
WebElementCoverage {
id: "e1".to_string(),
interaction_count: 1,
interaction_types: vec![],
},
);
coverage.html_elements.insert(
"e2".to_string(),
WebElementCoverage {
id: "e2".to_string(),
interaction_count: 0,
interaction_types: vec![],
},
);
coverage.css_rules.insert(
"#c1".to_string(),
RuleCoverage {
selector: "#c1".to_string(),
application_count: 1,
},
);
coverage.css_rules.insert(
"#c2".to_string(),
RuleCoverage {
selector: "#c2".to_string(),
application_count: 0,
},
);
coverage.js_functions.insert(
"f1".to_string(),
FunctionCoverage {
name: "f1".to_string(),
execution_count: 1,
},
);
coverage.js_functions.insert(
"f2".to_string(),
FunctionCoverage {
name: "f2".to_string(),
execution_count: 0,
},
);
assert_eq!(coverage.coverage_percent(), 50.0);
let report = coverage.report();
assert_eq!(report.html_coverage, 50.0);
assert_eq!(report.css_coverage, 50.0);
assert_eq!(report.js_coverage, 50.0);
assert_eq!(report.overall_coverage, 50.0);
}
#[test]
fn h0_web_12_bundle_with_invalid_js() {
let html = HtmlBuilder::new()
.title("Test")
.canvas("c", 100, 100)
.build()
.unwrap();
let css = CssBuilder::new().build().unwrap();
let js = GeneratedJs {
content: "eval('bad')".to_string(),
line_count: 1,
functions: vec![],
};
let bundle = WebBundle::new(html, css, js);
assert!(!bundle.is_valid());
}
#[test]
fn h0_web_13_single_file_contains_all_parts() {
let html = HtmlBuilder::new()
.title("My Game")
.canvas("game-canvas", 640, 480)
.build()
.unwrap();
let css = CssBuilder::new()
.variable("main-color", "#ff0000")
.reset()
.build()
.unwrap();
let js = JsBuilder::new("game.wasm", "game-canvas")
.memory(512, 2048)
.build()
.unwrap();
let bundle = WebBundle::new(html, css, js);
let output = bundle.to_single_file();
assert!(output.contains("<html lang=\"en\">"));
assert!(output.contains("<meta charset=\"UTF-8\">"));
assert!(output.contains("<title>My Game</title>"));
assert!(output.contains("</html>"));
assert!(output.contains("<style>"));
assert!(output.contains("</style>"));
assert!(output.contains("<script>"));
assert!(output.contains("</script>"));
assert!(output.contains("game.wasm"));
}
#[test]
fn h0_web_14_web_element_coverage_default() {
let elem: WebElementCoverage = Default::default();
assert!(elem.id.is_empty());
assert_eq!(elem.interaction_count, 0);
assert!(elem.interaction_types.is_empty());
}
#[test]
fn h0_web_15_rule_coverage_default() {
let rule: RuleCoverage = Default::default();
assert!(rule.selector.is_empty());
assert_eq!(rule.application_count, 0);
}
#[test]
fn h0_web_16_function_coverage_default() {
let func: FunctionCoverage = Default::default();
assert!(func.name.is_empty());
assert_eq!(func.execution_count, 0);
}
#[test]
fn h0_web_17_coverage_report_all_fields() {
let mut coverage = WebAssetCoverage::new();
coverage.html_elements.insert(
"elem".to_string(),
WebElementCoverage {
id: "elem".to_string(),
interaction_count: 1,
interaction_types: vec!["click".to_string()],
},
);
let report = coverage.report();
assert_eq!(report.html_coverage, 100.0);
assert_eq!(report.css_coverage, 100.0); assert_eq!(report.js_coverage, 100.0); assert_eq!(report.overall_coverage, 100.0);
}
#[test]
fn h0_web_18_element_used_creates_new_entry() {
let mut coverage = WebAssetCoverage::new();
assert!(coverage.html_elements.is_empty());
coverage.element_used("new-elem", "focus");
assert_eq!(coverage.html_elements.len(), 1);
let elem = coverage.html_elements.get("new-elem").unwrap();
assert_eq!(elem.id, "new-elem");
assert_eq!(elem.interaction_count, 1);
assert_eq!(elem.interaction_types, vec!["focus".to_string()]);
}
#[test]
fn h0_web_19_web_bundle_clone() {
let html = HtmlBuilder::new()
.title("Clone Test")
.canvas("c", 100, 100)
.build()
.unwrap();
let css = CssBuilder::new().build().unwrap();
let js = JsBuilder::new("app.wasm", "c").build().unwrap();
let bundle = WebBundle::new(html, css, js);
let cloned = bundle.clone();
assert_eq!(cloned.html.title, bundle.html.title);
assert_eq!(cloned.css.content, bundle.css.content);
assert_eq!(cloned.js.content, bundle.js.content);
}
#[test]
fn h0_web_20_web_asset_coverage_clone() {
let mut coverage = WebAssetCoverage::new();
coverage.element_used("test", "click");
coverage.rule_applied("#test");
coverage.function_executed("main");
let cloned = coverage.clone();
assert_eq!(cloned.html_elements.len(), coverage.html_elements.len());
assert_eq!(cloned.css_rules.len(), coverage.css_rules.len());
assert_eq!(cloned.js_functions.len(), coverage.js_functions.len());
}
#[test]
fn h0_web_21_coverage_report_clone() {
let report = WebAssetCoverageReport {
html_coverage: 75.0,
css_coverage: 80.0,
js_coverage: 90.0,
overall_coverage: 81.67,
};
let cloned = report.clone();
assert_eq!(cloned.html_coverage, report.html_coverage);
assert_eq!(cloned.css_coverage, report.css_coverage);
assert_eq!(cloned.js_coverage, report.js_coverage);
assert_eq!(cloned.overall_coverage, report.overall_coverage);
}
#[test]
fn h0_web_22_debug_formatting() {
let coverage = WebAssetCoverage::new();
let debug_str = format!("{:?}", coverage);
assert!(debug_str.contains("WebAssetCoverage"));
let elem_coverage = WebElementCoverage::default();
let debug_str = format!("{:?}", elem_coverage);
assert!(debug_str.contains("WebElementCoverage"));
let rule_coverage = RuleCoverage::default();
let debug_str = format!("{:?}", rule_coverage);
assert!(debug_str.contains("RuleCoverage"));
let func_coverage = FunctionCoverage::default();
let debug_str = format!("{:?}", func_coverage);
assert!(debug_str.contains("FunctionCoverage"));
}
#[test]
fn h0_web_23_coverage_report_debug() {
let report = WebAssetCoverageReport {
html_coverage: 100.0,
css_coverage: 100.0,
js_coverage: 100.0,
overall_coverage: 100.0,
};
let debug_str = format!("{:?}", report);
assert!(debug_str.contains("WebAssetCoverageReport"));
assert!(debug_str.contains("100"));
}
#[test]
fn h0_web_24_coverage_percent_all_covered() {
let mut coverage = WebAssetCoverage::new();
coverage.html_elements.insert(
"e".to_string(),
WebElementCoverage {
id: "e".to_string(),
interaction_count: 1,
interaction_types: vec![],
},
);
coverage.css_rules.insert(
"#c".to_string(),
RuleCoverage {
selector: "#c".to_string(),
application_count: 1,
},
);
coverage.js_functions.insert(
"f".to_string(),
FunctionCoverage {
name: "f".to_string(),
execution_count: 1,
},
);
assert_eq!(coverage.coverage_percent(), 100.0);
}
#[test]
fn h0_web_25_coverage_percent_none_covered() {
let mut coverage = WebAssetCoverage::new();
coverage.html_elements.insert(
"e".to_string(),
WebElementCoverage {
id: "e".to_string(),
interaction_count: 0,
interaction_types: vec![],
},
);
coverage.css_rules.insert(
"#c".to_string(),
RuleCoverage {
selector: "#c".to_string(),
application_count: 0,
},
);
coverage.js_functions.insert(
"f".to_string(),
FunctionCoverage {
name: "f".to_string(),
execution_count: 0,
},
);
assert_eq!(coverage.coverage_percent(), 0.0);
}
#[test]
fn h0_web_26_bundle_validation_reflects_html_errors() {
let html = GeneratedHtml {
title: "Test".to_string(),
body_content: String::new(),
content: "<html><head><title>Test</title></head><body></body></html>".to_string(),
elements: vec![],
};
let css = CssBuilder::new().build().unwrap();
let js = JsBuilder::new("app.wasm", "c").build().unwrap();
let bundle = WebBundle::new(html, css, js);
assert!(!bundle.is_valid());
assert!(!bundle.validation.html.is_valid());
}
#[test]
fn h0_web_27_html_coverage_zero_items() {
let coverage = WebAssetCoverage::new();
let report = coverage.report();
assert_eq!(report.html_coverage, 100.0);
}
#[test]
fn h0_web_28_css_coverage_zero_items() {
let coverage = WebAssetCoverage::new();
let report = coverage.report();
assert_eq!(report.css_coverage, 100.0);
}
#[test]
fn h0_web_29_js_coverage_zero_items() {
let coverage = WebAssetCoverage::new();
let report = coverage.report();
assert_eq!(report.js_coverage, 100.0);
}
#[test]
fn h0_web_30_multiple_functions_executed() {
let mut coverage = WebAssetCoverage::new();
coverage.function_executed("init");
coverage.function_executed("update");
coverage.function_executed("render");
coverage.function_executed("init");
assert_eq!(coverage.js_functions.len(), 3);
assert_eq!(
coverage.js_functions.get("init").unwrap().execution_count,
2
);
assert_eq!(
coverage.js_functions.get("update").unwrap().execution_count,
1
);
assert_eq!(
coverage.js_functions.get("render").unwrap().execution_count,
1
);
}
}