Add comprehensive testing infrastructure: visual snapshots, unit, property, and theme tests
Introduces a multi-layered testing strategy for the libcosmic widget toolkit: - Visual snapshot testing: Renders widgets via tiny-skia software renderer and compares pixel output against reference PNGs. Generates diff images on failure. Covers containers, dividers, text styles, progress bars, and layouts. - Widget unit tests: Thorough behavioral tests for segmented button model (selection, ordering, data storage, enable/disable, closable, indent), calendar model (month navigation, year wrapping, date selection, grid layout), spin button (clamping, increment/decrement, float support), and color picker. - Property-based tests (proptest): Randomized input testing for spin button bounds invariants, calendar first-day-of-week correctness, segmented model length/clear/toggle invariants, and theme spacing monotonicity. - Theme consistency tests: Validates all four theme variants (dark, light, high-contrast dark/light) have valid spacing, corner radii ordering, non-zero-alpha colors, visible text-on-background, and RON round-trip serialization. - Expanded inline tests: 16 new spin_button unit tests covering increment, decrement, clamping, float types, and constructor value bounds. - CI integration: New visual-tests job runs snapshot, theme, property, and widget tests with artifact upload on failure for snapshot diffs. https://claude.ai/code/session_01P3WLXMif9wmNXbWDo5FhRQ
This commit is contained in:
parent
3e78eb2381
commit
d25a51bd32
17 changed files with 1788 additions and 2 deletions
183
tests/visual_tests.rs
Normal file
183
tests/visual_tests.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Visual snapshot tests for COSMIC widgets.
|
||||
//!
|
||||
//! These tests render widgets using the tiny-skia software renderer and compare
|
||||
//! the output against stored reference snapshots. This catches unintended visual
|
||||
//! regressions in widget rendering, theming, and layout.
|
||||
//!
|
||||
//! # Updating Snapshots
|
||||
//!
|
||||
//! When a visual change is intentional, delete the old snapshot PNG from
|
||||
//! `tests/snapshots/` and re-run the tests. The new reference will be generated
|
||||
//! automatically.
|
||||
|
||||
mod snapshot_harness;
|
||||
|
||||
use cosmic::iced_core::Length;
|
||||
use cosmic::theme;
|
||||
use snapshot_harness::SnapshotHarness;
|
||||
|
||||
/// Helper to render a cosmic Element to a snapshot.
|
||||
fn render_element_snapshot(
|
||||
name: &str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
element: cosmic::Element<'_, ()>,
|
||||
) {
|
||||
let harness = SnapshotHarness::new(width, height);
|
||||
harness.snapshot(name, element);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme rendering tests - verify that the cosmic theme system produces
|
||||
// consistent visual output for core UI primitives.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn snapshot_container_with_background() {
|
||||
use cosmic::widget::{container, text};
|
||||
|
||||
let content = text::body("Hello, COSMIC!")
|
||||
.apply(container)
|
||||
.padding(16)
|
||||
.width(Length::Fixed(200.0))
|
||||
.height(Length::Fixed(60.0))
|
||||
.class(theme::Container::Primary);
|
||||
|
||||
render_element_snapshot("container_with_background", 220, 80, content.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_divider_horizontal() {
|
||||
use cosmic::widget::{column, container, divider};
|
||||
|
||||
let content = column::with_capacity(3)
|
||||
.push(cosmic::widget::text::body("Above"))
|
||||
.push(divider::horizontal::default())
|
||||
.push(cosmic::widget::text::body("Below"))
|
||||
.width(Length::Fixed(200.0))
|
||||
.apply(container)
|
||||
.padding(8);
|
||||
|
||||
render_element_snapshot("divider_horizontal", 220, 100, content.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_divider_heavy() {
|
||||
use cosmic::widget::{column, container, divider};
|
||||
|
||||
let content = column::with_capacity(3)
|
||||
.push(cosmic::widget::text::body("Above"))
|
||||
.push(divider::horizontal::heavy())
|
||||
.push(cosmic::widget::text::body("Below"))
|
||||
.width(Length::Fixed(200.0))
|
||||
.apply(container)
|
||||
.padding(8);
|
||||
|
||||
render_element_snapshot("divider_heavy", 220, 100, content.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_text_styles() {
|
||||
use cosmic::widget::{column, container, text};
|
||||
|
||||
let content = column::with_capacity(4)
|
||||
.push(text::title3("Title Text"))
|
||||
.push(text::body("Body text content"))
|
||||
.push(text::caption("Caption text"))
|
||||
.push(text::monotext("Monospace text"))
|
||||
.spacing(8)
|
||||
.width(Length::Fixed(300.0))
|
||||
.apply(container)
|
||||
.padding(16);
|
||||
|
||||
render_element_snapshot("text_styles", 340, 200, content.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_progress_bar() {
|
||||
use cosmic::widget::{column, container, progress_bar};
|
||||
|
||||
let content = column::with_capacity(3)
|
||||
.push(progress_bar(0.0..=100.0, 0.0).width(Length::Fixed(250.0)))
|
||||
.push(progress_bar(0.0..=100.0, 50.0).width(Length::Fixed(250.0)))
|
||||
.push(progress_bar(0.0..=100.0, 100.0).width(Length::Fixed(250.0)))
|
||||
.spacing(12)
|
||||
.apply(container)
|
||||
.padding(16);
|
||||
|
||||
render_element_snapshot("progress_bar_states", 290, 120, content.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_row_layout() {
|
||||
use cosmic::widget::{container, row, text};
|
||||
|
||||
let content = row::with_capacity(3)
|
||||
.push(text::body("Left"))
|
||||
.push(text::body("Center"))
|
||||
.push(text::body("Right"))
|
||||
.spacing(16)
|
||||
.apply(container)
|
||||
.padding(12);
|
||||
|
||||
render_element_snapshot("row_layout", 300, 60, content.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_column_layout() {
|
||||
use cosmic::widget::{column, container, text};
|
||||
|
||||
let content = column::with_capacity(3)
|
||||
.push(text::body("First"))
|
||||
.push(text::body("Second"))
|
||||
.push(text::body("Third"))
|
||||
.spacing(8)
|
||||
.apply(container)
|
||||
.padding(12);
|
||||
|
||||
render_element_snapshot("column_layout", 200, 120, content.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_nested_containers() {
|
||||
use cosmic::widget::{column, container, row, text};
|
||||
|
||||
let inner = column::with_capacity(2)
|
||||
.push(text::body("Nested"))
|
||||
.push(text::caption("Content"))
|
||||
.spacing(4)
|
||||
.apply(container)
|
||||
.padding(8)
|
||||
.class(theme::Container::Primary);
|
||||
|
||||
let outer = row::with_capacity(2)
|
||||
.push(inner)
|
||||
.push(text::body("Adjacent"))
|
||||
.spacing(16)
|
||||
.apply(container)
|
||||
.padding(12);
|
||||
|
||||
render_element_snapshot("nested_containers", 300, 120, outer.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_dark_theme_container() {
|
||||
use cosmic::widget::{container, text};
|
||||
|
||||
// Set dark theme temporarily for this test
|
||||
let content = text::body("Dark Theme")
|
||||
.apply(container)
|
||||
.padding(16)
|
||||
.width(Length::Fixed(200.0))
|
||||
.height(Length::Fixed(60.0))
|
||||
.class(theme::Container::Primary);
|
||||
|
||||
// The default theme is dark, so this tests the default rendering
|
||||
render_element_snapshot("dark_theme_container", 220, 80, content.into());
|
||||
}
|
||||
|
||||
// We use `apply` from the Apply trait for chaining
|
||||
use apply::Apply;
|
||||
Loading…
Add table
Add a link
Reference in a new issue