Files
rust_browser/tests/goldens.rs
Zachary D. Rowitsch dbadb659bc Add CSS cascade origin ordering per CSS Cascading Level 4
Author-origin declarations now always beat UA-origin declarations
regardless of specificity, fixing issues like `.btn-primary { color: white }`
failing to override UA `a:link { color: #0000EE }`. The cascade now checks
origin+importance weight before specificity: UA normal (0) < Author normal (1)
< Author !important (2) < UA !important (3).

The UA stylesheet is applied internally by compute_styles rather than being
prepended by callers, making origin structurally determined. Promotes 3 WPT
tests from known_fail to pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:35:44 -05:00

1063 lines
22 KiB
Rust

//! Golden tests for the rendering pipeline.
//!
//! These tests compare the output of the rendering pipeline against
//! expected "golden" files. If the output differs, the test fails.
//!
//! To update golden files when intentionally changing output:
//! ```
//! for f in tests/goldens/fixtures/*.html; do
//! cargo run -p app_browser -- --render "$f" --output-dir tests/goldens/expected
//! done
//! ```
use std::fs;
use std::path::{Path, PathBuf};
// Re-export pipeline for tests
mod pipeline_test {
use display_list::build_display_list;
use html::HtmlParser;
use image::{ImagePipeline, ImageStore};
use layout::LayoutEngine;
use std::collections::HashMap;
use std::path::Path;
use style::{Display, StyleContext};
pub fn run_pipeline(html: &str) -> (String, String) {
let html_parser = HtmlParser::new();
let style_context = StyleContext::new();
let layout_engine = LayoutEngine::new_proportional();
let document = html_parser.parse(html);
// Include UA stylesheet first (lowest priority), then author stylesheets
let stylesheets = style_context.extract_stylesheets(&document);
let inline_styles = style_context.extract_inline_styles(&document);
let computed_styles = style_context.compute_styles(
&document,
&stylesheets,
&inline_styles,
Some(&css::Viewport::new(800.0, 600.0)),
);
let layout_tree = layout_engine.layout(&document, &computed_styles, 800.0, 600.0);
let display_list = build_display_list(&layout_tree);
(layout_tree.dump(), display_list.dump())
}
/// Run the pipeline with image loading from a fixture directory.
/// Resolves `<img src="...">` paths relative to `fixture_dir`.
pub fn run_pipeline_with_images(html: &str, fixture_dir: &Path) -> (String, String) {
let html_parser = HtmlParser::new();
let style_context = StyleContext::new();
let layout_engine = LayoutEngine::new_proportional();
let image_pipeline = ImagePipeline::new();
let document = html_parser.parse(html);
let stylesheets = style_context.extract_stylesheets(&document);
let inline_styles = style_context.extract_inline_styles(&document);
let computed_styles = style_context.compute_styles(
&document,
&stylesheets,
&inline_styles,
Some(&css::Viewport::new(800.0, 600.0)),
);
// Load images from <img> elements
let mut image_store = ImageStore::new();
let mut image_map = HashMap::new();
for node_id in document.get_elements_by_tag_name("img") {
// Skip display:none elements
if let Some(styles) = computed_styles.get(&node_id) {
if styles.display == Display::None {
continue;
}
}
if let Some(src) = document.get_attribute(node_id, "src") {
let image_path = fixture_dir.join(src);
if let Ok(bytes) = std::fs::read(&image_path) {
if let Ok(decoded) = image_pipeline.decode(&bytes, None) {
let img_id = image_store.insert(decoded);
image_map.insert(node_id, img_id);
}
}
}
}
let layout_tree = layout_engine.layout_with_images(
&document,
&computed_styles,
800.0,
600.0,
&image_map,
&image_store,
);
let display_list = build_display_list(&layout_tree);
(layout_tree.dump(), display_list.dump())
}
}
fn get_goldens_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/goldens")
}
fn compare_golden_outputs(
fixture_name: &str,
actual_layout: &str,
actual_dl: &str,
expected_dir: &Path,
) {
let expected_layout_path = expected_dir.join(format!("{}.layout.txt", fixture_name));
let expected_dl_path = expected_dir.join(format!("{}.dl.txt", fixture_name));
let expected_layout = fs::read_to_string(&expected_layout_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", expected_layout_path.display(), e));
let expected_dl = fs::read_to_string(&expected_dl_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", expected_dl_path.display(), e));
if actual_layout != expected_layout {
panic!(
"Layout mismatch for {}!\n\
=== Expected ===\n{}\n\
=== Actual ===\n{}\n",
fixture_name, expected_layout, actual_layout
);
}
if actual_dl != expected_dl {
panic!(
"Display list mismatch for {}!\n\
=== Expected ===\n{}\n\
=== Actual ===\n{}\n",
fixture_name, expected_dl, actual_dl
);
}
}
fn run_golden_test(fixture_name: &str) {
let goldens_dir = get_goldens_dir();
let fixtures_dir = goldens_dir.join("fixtures");
let expected_dir = goldens_dir.join("expected");
let html_path = fixtures_dir.join(format!("{}.html", fixture_name));
let html = fs::read_to_string(&html_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", html_path.display(), e));
let (actual_layout, actual_dl) = pipeline_test::run_pipeline(&html);
compare_golden_outputs(fixture_name, &actual_layout, &actual_dl, &expected_dir);
}
fn run_golden_test_with_images(fixture_name: &str) {
let goldens_dir = get_goldens_dir();
let fixtures_dir = goldens_dir.join("fixtures");
let expected_dir = goldens_dir.join("expected");
let html_path = fixtures_dir.join(format!("{}.html", fixture_name));
let html = fs::read_to_string(&html_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", html_path.display(), e));
let (actual_layout, actual_dl) = pipeline_test::run_pipeline_with_images(&html, &fixtures_dir);
compare_golden_outputs(fixture_name, &actual_layout, &actual_dl, &expected_dir);
}
#[test]
fn golden_001_empty() {
run_golden_test("001-empty");
}
#[test]
fn golden_002_single_div() {
run_golden_test("002-single-div");
}
#[test]
fn golden_003_nested_divs() {
run_golden_test("003-nested-divs");
}
#[test]
fn golden_004_text_content() {
run_golden_test("004-text-content");
}
#[test]
fn golden_005_inline_style() {
run_golden_test("005-inline-style");
}
#[test]
fn golden_006_style_tag() {
run_golden_test("006-style-tag");
}
#[test]
fn golden_007_class_selector() {
run_golden_test("007-class-selector");
}
#[test]
fn golden_008_id_selector() {
run_golden_test("008-id-selector");
}
#[test]
fn golden_009_margin_padding() {
run_golden_test("009-margin-padding");
}
#[test]
fn golden_010_block_layout() {
run_golden_test("010-block-layout");
}
#[test]
fn golden_011_width_height() {
run_golden_test("011-width-height");
}
#[test]
fn golden_012_background_color() {
run_golden_test("012-background-color");
}
#[test]
fn golden_013_border_color() {
run_golden_test("013-border-color");
}
#[test]
fn golden_014_border_style_none() {
run_golden_test("014-border-style-none");
}
#[test]
fn golden_015_border_shorthand() {
run_golden_test("015-border-shorthand");
}
#[test]
fn golden_016_border_per_side() {
run_golden_test("016-border-per-side");
}
// Phase 1B: Inline Layout Tests
#[test]
fn golden_017_inline_span() {
run_golden_test("017-inline-span");
}
#[test]
fn golden_018_multiple_inline() {
run_golden_test("018-multiple-inline");
}
#[test]
fn golden_019_text_wrapping() {
run_golden_test("019-text-wrapping");
}
#[test]
fn golden_020_mixed_content() {
run_golden_test("020-mixed-content");
}
#[test]
fn golden_021_nested_inline() {
run_golden_test("021-nested-inline");
}
#[test]
fn golden_022_inline_background_border() {
run_golden_test("022-inline-background-border");
}
#[test]
fn golden_023_nested_inline_styles() {
run_golden_test("023-nested-inline-styles");
}
#[test]
fn golden_024_inline_border_wrap() {
run_golden_test("024-inline-border-wrap");
}
// Phase 1D: Golden Test Expansion (Edge Cases)
#[test]
fn golden_025_empty_inline() {
run_golden_test("025-empty-inline");
}
#[test]
fn golden_026_whitespace_only_inline() {
run_golden_test("026-whitespace-only-inline");
}
#[test]
fn golden_027_unbreakable_long_word() {
run_golden_test("027-unbreakable-long-word");
}
#[test]
fn golden_028_inline_at_block_start() {
run_golden_test("028-inline-at-block-start");
}
#[test]
fn golden_029_inline_at_block_end() {
run_golden_test("029-inline-at-block-end");
}
#[test]
fn golden_030_adjacent_inline_no_space() {
run_golden_test("030-adjacent-inline-no-space");
}
#[test]
fn golden_031_different_border_widths() {
run_golden_test("031-different-border-widths");
}
#[test]
fn golden_032_full_box_model() {
run_golden_test("032-full-box-model");
}
#[test]
fn golden_033_zero_width_border() {
run_golden_test("033-zero-width-border");
}
#[test]
fn golden_034_deeply_nested_inline() {
run_golden_test("034-deeply-nested-inline");
}
#[test]
fn golden_035_multiple_blocks_interrupting() {
run_golden_test("035-multiple-blocks-interrupting");
}
#[test]
fn golden_036_empty_nested_inline() {
run_golden_test("036-empty-nested-inline");
}
#[test]
fn golden_037_entities() {
run_golden_test("037-entities");
}
#[test]
fn golden_038_inline_link() {
run_golden_test("038-inline-link");
}
// Phase 2: Navigation Milestone Tests
#[test]
fn golden_039_anchor_link() {
run_golden_test("039-anchor-link");
}
#[test]
fn golden_040_multiple_anchors() {
run_golden_test("040-multiple-anchors");
}
#[test]
fn golden_041_anchor_with_style() {
run_golden_test("041-anchor-with-style");
}
#[test]
fn golden_042_nested_anchor() {
run_golden_test("042-nested-anchor");
}
#[test]
fn golden_043_heading_levels() {
run_golden_test("043-heading-levels");
}
#[test]
fn golden_044_paragraph_text() {
run_golden_test("044-paragraph-text");
}
#[test]
fn golden_045_mixed_headings_para() {
run_golden_test("045-mixed-headings-para");
}
#[test]
fn golden_046_list_like_divs() {
run_golden_test("046-list-like-divs");
}
#[test]
fn golden_047_wide_content() {
run_golden_test("047-wide-content");
}
#[test]
fn golden_048_tall_content() {
run_golden_test("048-tall-content");
}
#[test]
fn golden_049_test2() {
run_golden_test("049-css-width-percentage");
}
// Milestone 3: Expand CSS for Real Pages - Phase 1: Selector Validation
#[test]
fn golden_050_descendant_deep() {
run_golden_test("050-descendant-deep");
}
#[test]
fn golden_051_child_vs_descendant() {
run_golden_test("051-child-vs-descendant");
}
// Milestone 3: Phase 2 - Position Relative/Absolute
#[test]
fn golden_052_position_relative() {
run_golden_test("052-position-relative");
}
#[test]
fn golden_053_position_relative_offset() {
run_golden_test("053-position-relative-offset");
}
#[test]
fn golden_054_position_absolute() {
run_golden_test("054-position-absolute");
}
#[test]
fn golden_055_absolute_in_relative() {
run_golden_test("055-absolute-in-relative");
}
// Milestone 3: Phase 3 - Stacking Contexts & Z-Index
#[test]
fn golden_056_z_index_basic() {
run_golden_test("056-z-index-basic");
}
#[test]
fn golden_057_z_index_negative() {
run_golden_test("057-z-index-negative");
}
#[test]
fn golden_058_stacking_context() {
run_golden_test("058-stacking-context");
}
// Milestone 3: Phase 4 - Overflow & Clipping
#[test]
fn golden_059_overflow_hidden() {
run_golden_test("059-overflow-hidden");
}
#[test]
fn golden_060_overflow_nested() {
run_golden_test("060-overflow-nested");
}
// Milestone 3: Phase 5 - Flexbox Layout
#[test]
fn golden_061_flex_row() {
run_golden_test("061-flex-row");
}
#[test]
fn golden_062_flex_column() {
run_golden_test("062-flex-column");
}
#[test]
fn golden_063_flex_justify() {
run_golden_test("063-flex-justify");
}
#[test]
fn golden_064_flex_align() {
run_golden_test("064-flex-align");
}
#[test]
fn golden_065_flex_grow() {
run_golden_test("065-flex-grow");
}
#[test]
fn golden_066_flex_gap() {
run_golden_test("066-flex-gap");
}
// Phase 3: white-space property and <pre> tag support
#[test]
fn golden_067_pre_basic() {
run_golden_test("067-pre-basic");
}
#[test]
fn golden_068_pre_whitespace() {
run_golden_test("068-pre-whitespace");
}
#[test]
fn golden_069_white_space_pre_css() {
run_golden_test("069-white-space-pre-css");
}
#[test]
fn golden_070_white_space_nowrap() {
run_golden_test("070-white-space-nowrap");
}
#[test]
fn golden_071_white_space_pre_wrap() {
run_golden_test("071-white-space-pre-wrap");
}
#[test]
fn golden_072_white_space_pre_line() {
run_golden_test("072-white-space-pre-line");
}
#[test]
fn golden_073_pre_tabs() {
run_golden_test("073-pre-tabs");
}
#[test]
fn golden_074_pre_empty() {
run_golden_test("074-pre-empty");
}
#[test]
fn golden_075_pre_nested_inline() {
run_golden_test("075-pre-nested-inline");
}
#[test]
fn golden_076_pre_override_css() {
run_golden_test("076-pre-override-css");
}
#[test]
fn golden_077_pre_multiple_newlines() {
run_golden_test("077-pre-multiple-newlines");
}
// Phase: Table Layout Support
#[test]
fn golden_078_table_basic() {
run_golden_test("078-table-basic");
}
#[test]
fn golden_079_table_headers() {
run_golden_test("079-table-headers");
}
#[test]
fn golden_080_table_sections() {
run_golden_test("080-table-sections");
}
#[test]
fn golden_081_table_colspan() {
run_golden_test("081-table-colspan");
}
#[test]
fn golden_082_table_rowspan() {
run_golden_test("082-table-rowspan");
}
#[test]
fn golden_083_table_mixed_spans() {
run_golden_test("083-table-mixed-spans");
}
#[test]
fn golden_084_table_column_widths() {
run_golden_test("084-table-column-widths");
}
#[test]
fn golden_085_table_cell_padding() {
run_golden_test("085-table-cell-padding");
}
#[test]
fn golden_086_table_backgrounds() {
run_golden_test("086-table-backgrounds");
}
#[test]
fn golden_087_table_width() {
run_golden_test("087-table-width");
}
#[test]
fn golden_088_table_nested() {
run_golden_test("088-table-nested");
}
#[test]
fn golden_089_table_text_wrap() {
run_golden_test("089-table-text-wrap");
}
#[test]
fn golden_090_table_empty_cells() {
run_golden_test("090-table-empty-cells");
}
// XML/XHTML compatibility
#[test]
fn golden_091_xml_processing_instructions() {
run_golden_test("091-xml-processing-instructions");
}
// HTML parsing edge cases
#[test]
fn golden_092_unquoted_attr_with_slash() {
run_golden_test("092-unquoted-attr-with-slash");
}
// Border styles: dashed, dotted, CSS variables
#[test]
fn golden_093_dashed_borders() {
run_golden_test("093-dashed-borders");
}
// Table cell vertical-align
#[test]
fn golden_094_table_vertical_align() {
run_golden_test("094-table-vertical-align");
}
// CSS Feature Expansion: min/max dimensions, position: fixed, inline-block
#[test]
fn golden_095_max_width() {
run_golden_test("095-max-width");
}
#[test]
fn golden_096_min_height() {
run_golden_test("096-min-height");
}
#[test]
fn golden_097_position_fixed() {
run_golden_test("097-position-fixed");
}
#[test]
fn golden_098_inline_block_basic() {
run_golden_test("098-inline-block-basic");
}
#[test]
fn golden_099_min_max_width_conflict() {
run_golden_test("099-min-max-width-conflict");
}
#[test]
fn golden_100_fixed_nested() {
run_golden_test("100-fixed-nested");
}
#[test]
fn golden_101_inline_block_wrapping() {
run_golden_test("101-inline-block-wrapping");
}
#[test]
fn golden_102_inline_block_padding_margin() {
run_golden_test("102-inline-block-padding-margin");
}
#[test]
fn golden_103_max_width_percentage() {
run_golden_test("103-max-width-percentage");
}
#[test]
fn golden_104_fixed_with_scroll() {
run_golden_test("104-fixed-with-scroll");
}
// CSS Float Layout
#[test]
fn golden_105_float_left_basic() {
run_golden_test("105-float-left-basic");
}
#[test]
fn golden_106_float_right_basic() {
run_golden_test("106-float-right-basic");
}
#[test]
fn golden_107_float_multiple_left() {
run_golden_test("107-float-multiple-left");
}
#[test]
fn golden_108_float_clear_left() {
run_golden_test("108-float-clear-left");
}
#[test]
fn golden_109_float_clear_both() {
run_golden_test("109-float-clear-both");
}
#[test]
fn golden_110_float_overflow_contain() {
run_golden_test("110-float-overflow-contain");
}
#[test]
fn golden_111_float_no_parent_expand() {
run_golden_test("111-float-no-parent-expand");
}
#[test]
fn golden_112_float_inline_wrap() {
run_golden_test("112-float-inline-wrap");
}
#[test]
fn golden_113_float_drop_below() {
run_golden_test("113-float-drop-below");
}
#[test]
fn golden_114_float_with_margin() {
run_golden_test("114-float-with-margin");
}
#[test]
fn golden_115_float_with_padding_border() {
run_golden_test("115-float-with-padding-border");
}
#[test]
fn golden_116_float_percentage_width() {
run_golden_test("116-float-percentage-width");
}
#[test]
fn golden_117_float_next_to_inline_block() {
run_golden_test("117-float-next-to-inline-block");
}
#[test]
fn golden_118_float_with_clear_on_float() {
run_golden_test("118-float-with-clear-on-float");
}
#[test]
fn golden_119_float_empty() {
run_golden_test("119-float-empty");
}
#[test]
fn golden_120_float_mixed_left_right() {
run_golden_test("120-float-mixed-left-right");
}
#[test]
fn golden_121_float_nested_bfc() {
run_golden_test("121-float-nested-bfc");
}
#[test]
fn golden_122_float_with_text_content() {
run_golden_test("122-float-with-text-content");
}
#[test]
fn golden_123_float_stacking_multiple_heights() {
run_golden_test("123-float-stacking-multiple-heights");
}
#[test]
fn golden_124_float_clear_right() {
run_golden_test("124-float-clear-right");
}
// Milestone 4: Image Support
#[test]
fn golden_125_img_basic() {
run_golden_test_with_images("125-img-basic");
}
#[test]
fn golden_126_img_width_attr() {
run_golden_test_with_images("126-img-width-attr");
}
#[test]
fn golden_127_img_height_attr() {
run_golden_test_with_images("127-img-height-attr");
}
#[test]
fn golden_128_img_both_attrs() {
run_golden_test_with_images("128-img-both-attrs");
}
#[test]
fn golden_129_img_css_width() {
run_golden_test_with_images("129-img-css-width");
}
#[test]
fn golden_130_img_css_both() {
run_golden_test_with_images("130-img-css-both");
}
#[test]
fn golden_131_img_in_div() {
run_golden_test_with_images("131-img-in-div");
}
#[test]
fn golden_132_img_inline() {
run_golden_test_with_images("132-img-inline");
}
#[test]
fn golden_133_img_missing_src() {
run_golden_test_with_images("133-img-missing-src");
}
#[test]
fn golden_134_img_broken_src() {
run_golden_test_with_images("134-img-broken-src");
}
#[test]
fn golden_135_img_with_border() {
run_golden_test_with_images("135-img-with-border");
}
#[test]
fn golden_136_img_display_block() {
run_golden_test_with_images("136-img-display-block");
}
// Milestone 5: Font Properties
#[test]
fn golden_137_font_weight_bold() {
run_golden_test("137-font-weight-bold");
}
#[test]
fn golden_138_font_style_italic() {
run_golden_test("138-font-style-italic");
}
#[test]
fn golden_139_font_family_monospace() {
run_golden_test("139-font-family-monospace");
}
#[test]
fn golden_140_mixed_font_styles() {
run_golden_test("140-mixed-font-styles");
}
#[test]
fn golden_141_heading_font_weight() {
run_golden_test("141-heading-font-weight");
}
// Regression: Image inside inline wrapper (e.g., <font><img></font>)
#[test]
fn golden_142_img_in_inline_wrapper() {
run_golden_test_with_images("142-img-in-inline-wrapper");
}
#[test]
fn golden_143_img_link_in_table() {
run_golden_test_with_images("143-img-link-in-table");
}
#[test]
fn golden_144_grid_fixed_columns() {
run_golden_test("144-grid-fixed-columns");
}
#[test]
fn golden_145_grid_fr_units() {
run_golden_test("145-grid-fr-units");
}
#[test]
fn golden_146_grid_gap() {
run_golden_test("146-grid-gap");
}
#[test]
fn golden_147_grid_explicit_placement() {
run_golden_test("147-grid-explicit-placement");
}
#[test]
fn golden_148_grid_span() {
run_golden_test("148-grid-span");
}
// Float children positioning fix
#[test]
fn golden_149_float_right_with_block_children() {
run_golden_test("149-float-right-with-block-children");
}
// Flex container Y positioning + CSS min() in grid templates
#[test]
fn golden_150_flex_nav_in_grid() {
run_golden_test("150-flex-nav-in-grid");
}
// Grid items with explicit column but auto row should not overlap earlier rows
#[test]
fn golden_151_grid_explicit_col_auto_row() {
run_golden_test("151-grid-explicit-col-auto-row");
}
// Negative grid line numbers: 1/-1 spans all columns, 2/-1 spans last two
#[test]
fn golden_152_grid_negative_line_numbers() {
run_golden_test("152-grid-negative-line-numbers");
}
// List marker support
#[test]
fn golden_153_unordered_list() {
run_golden_test("153-unordered-list");
}
#[test]
fn golden_154_ordered_list() {
run_golden_test("154-ordered-list");
}
#[test]
fn golden_155_ordered_list_start() {
run_golden_test("155-ordered-list-start");
}
#[test]
fn golden_156_nested_lists() {
run_golden_test("156-nested-lists");
}
#[test]
fn golden_157_ordered_list_type() {
run_golden_test("157-ordered-list-type");
}
// Table with caption regression: captions must not cause row groups to be
// wrapped in anonymous blocks (which hides them from the table layout algorithm)
#[test]
fn golden_158_table_with_caption() {
run_golden_test("158-table-with-caption");
}
// Flexbox comprehensive tests
#[test]
fn golden_159_flex_wrap() {
run_golden_test("159-flex-wrap");
}
#[test]
fn golden_160_flex_direction_reverse() {
run_golden_test("160-flex-direction-reverse");
}
#[test]
fn golden_161_flex_align_self() {
run_golden_test("161-flex-align-self");
}
#[test]
fn golden_162_flex_shorthand() {
run_golden_test("162-flex-shorthand");
}
#[test]
fn golden_163_flex_order() {
run_golden_test("163-flex-order");
}
#[test]
fn golden_164_inline_flex() {
run_golden_test("164-inline-flex");
}
#[test]
fn golden_165_justify_content_space_evenly() {
run_golden_test("165-justify-content-space-evenly");
}
#[test]
fn golden_166_box_sizing() {
run_golden_test("166-box-sizing");
}
#[test]
fn golden_167_box_sizing_comprehensive() {
run_golden_test("167-box-sizing-comprehensive");
}
#[test]
fn golden_168_bootstrap_fixed_top() {
run_golden_test("168-bootstrap-fixed-top");
}
#[test]
fn golden_169_img_svg_basic() {
run_golden_test_with_images("169-img-svg-basic");
}