Files
eepp/src/tests/unit_tests/uihtml_grid_test.cpp
Martín Lucas Golini ad4e4285d6 Grid fixes.
2026-06-02 12:29:37 -03:00

1868 lines
76 KiB
C++

#include "utest.h"
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/ui/css/stylesheetparser.hpp>
#include <eepp/ui/css/stylesheetspecification.hpp>
#include <eepp/ui/gridlayouter.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitextnode.hpp>
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/window.hpp>
using namespace EE;
using namespace EE::UI;
using namespace EE::UI::Tools;
using namespace EE::Window;
using namespace EE::Graphics;
static void init_grid_test() {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
FontFamily::loadFromRegular( font );
UISceneNode* sceneNode = UISceneNode::New();
SceneManager::instance()->add( sceneNode );
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
UIThemeManager* themeManager = sceneNode->getUIThemeManager();
themeManager->setDefaultFont( font );
themeManager->applyDefaultTheme( sceneNode->getRoot() );
}
// ─────────────────────────────────────────────────────────────────────────────
// Phase 0: Display routing and property defaults
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridProperties, displayGridEnumConversions ) {
EXPECT_TRUE( CSSDisplayHelper::toString( CSSDisplay::Grid ) == "grid" );
EXPECT_TRUE( CSSDisplayHelper::toString( CSSDisplay::InlineGrid ) == "inline-grid" );
EXPECT_TRUE( CSSDisplayHelper::fromString( "grid" ) == CSSDisplay::Grid );
EXPECT_TRUE( CSSDisplayHelper::fromString( "inline-grid" ) == CSSDisplay::InlineGrid );
}
UTEST( GridProperties, longhandDefaults ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
EXPECT_TRUE( grid->getGridTemplateRows() == "none" );
EXPECT_TRUE( grid->getGridTemplateColumns() == "none" );
EXPECT_TRUE( grid->getGridTemplateAreas() == "none" );
EXPECT_TRUE( grid->getGridAutoRows() == "auto" );
EXPECT_TRUE( grid->getGridAutoColumns() == "auto" );
EXPECT_EQ( grid->getGridAutoFlow(), CSSGridAutoFlow::Row );
EXPECT_EQ( grid->getGridAutoFlowDense(), false );
EXPECT_TRUE( grid->getGridRowStart() == "auto" );
EXPECT_TRUE( grid->getGridRowEnd() == "auto" );
EXPECT_TRUE( grid->getGridColumnStart() == "auto" );
EXPECT_TRUE( grid->getGridColumnEnd() == "auto" );
EXPECT_TRUE( grid->getGridArea() == "auto" );
EXPECT_EQ( grid->getJustifyItems(), CSSJustifyItems::Normal );
EXPECT_EQ( grid->getJustifySelf(), CSSJustifySelf::Auto );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Phase 4: Definite Placement (GridLineParser)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridLineParser, parseAuto ) {
GridLineResult r = GridLineParser::parse( "auto" );
EXPECT_TRUE( r.isAuto );
EXPECT_FALSE( r.isSpan );
}
UTEST( GridLineParser, parseInteger ) {
GridLineResult r = GridLineParser::parse( "3" );
EXPECT_FALSE( r.isAuto );
EXPECT_EQ( r.lineNumber, 3 );
}
UTEST( GridLineParser, parseNegativeInteger ) {
GridLineResult r = GridLineParser::parse( "-1" );
EXPECT_FALSE( r.isAuto );
EXPECT_EQ( r.lineNumber, -1 );
}
UTEST( GridLineParser, parseSpanCount ) {
GridLineResult r = GridLineParser::parse( "span 2" );
EXPECT_TRUE( r.isSpan );
EXPECT_EQ( r.spanCount, 2 );
}
UTEST( GridLineParser, parseEmpty ) {
GridLineResult r = GridLineParser::parse( "" );
EXPECT_TRUE( r.isAuto );
}
UTEST( GridPlacement, columnExplicitLines ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* item = UIHTMLWidget::New();
item->setParent( grid );
item->setGridColumnStart( "1" );
item->setGridColumnEnd( "3" );
item->setGridRowStart( "1" );
item->setGridRowEnd( "2" );
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( item->getParent() == grid );
Engine::destroySingleton();
}
UTEST( GridPlacement, usesSpanKeyword ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* item = UIHTMLWidget::New();
item->setParent( grid );
item->setGridColumnStart( "1" );
item->setGridColumnEnd( "span 2" );
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( item->getParent() == grid );
Engine::destroySingleton();
}
UTEST( GridPlacement, negativeEndLine ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* item = UIHTMLWidget::New();
item->setParent( grid );
item->setGridColumnStart( "1" );
item->setGridColumnEnd( "-1" );
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( item->getParent() == grid );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Phase 5-6: Auto-Placement
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridAutoPlacement, rowFlowFillsColumnsThenRows ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* i1 = UIHTMLWidget::New();
i1->setParent( grid );
UIHTMLWidget* i2 = UIHTMLWidget::New();
i2->setParent( grid );
UIHTMLWidget* i3 = UIHTMLWidget::New();
i3->setParent( grid );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 3u );
// All items should be placed (non-zero positions)
EXPECT_NE( items[0].resolvedRowStart, 0 );
EXPECT_NE( items[1].resolvedRowStart, 0 );
EXPECT_NE( items[2].resolvedRowStart, 0 );
EXPECT_NE( items[0].resolvedColumnStart, 0 );
EXPECT_NE( items[1].resolvedColumnStart, 0 );
EXPECT_NE( items[2].resolvedColumnStart, 0 );
// No overlapping positions
for ( size_t a = 0; a < items.size(); ++a )
for ( size_t b = a + 1; b < items.size(); ++b )
EXPECT_FALSE( items[a].resolvedRowStart == items[b].resolvedRowStart &&
items[a].resolvedColumnStart == items[b].resolvedColumnStart );
Engine::destroySingleton();
}
UTEST( GridAutoPlacement, columnFlowFillsRowsThenColumns ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateRows( "50px 50px" );
grid->setGridAutoFlow( CSSGridAutoFlow::Column );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* i1 = UIHTMLWidget::New();
i1->setParent( grid );
UIHTMLWidget* i2 = UIHTMLWidget::New();
i2->setParent( grid );
UIHTMLWidget* i3 = UIHTMLWidget::New();
i3->setParent( grid );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 3u );
EXPECT_EQ( items[0].resolvedRowStart, 1 );
EXPECT_EQ( items[0].resolvedColumnStart, 1 );
EXPECT_EQ( items[1].resolvedRowStart, 2 );
EXPECT_EQ( items[1].resolvedColumnStart, 1 );
EXPECT_EQ( items[2].resolvedRowStart, 1 );
EXPECT_EQ( items[2].resolvedColumnStart, 2 );
Engine::destroySingleton();
}
UTEST( GridAutoPlacement, definiteLeavesHoleForSparse ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Place item at (1,1)
UIHTMLWidget* i1 = UIHTMLWidget::New();
i1->setParent( grid );
i1->setGridColumnStart( "1" );
i1->setGridRowStart( "1" );
UIHTMLWidget* i2 = UIHTMLWidget::New();
i2->setParent( grid );
UIHTMLWidget* i3 = UIHTMLWidget::New();
i3->setParent( grid );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 3u );
// i1 definitely at (1,1)
EXPECT_EQ( items[0].resolvedRowStart, 1 );
EXPECT_EQ( items[0].resolvedColumnStart, 1 );
// i2 and i3 should not overlap with i1 or each other
for ( size_t a = 0; a < items.size(); ++a )
for ( size_t b = a + 1; b < items.size(); ++b )
EXPECT_FALSE( items[a].resolvedRowStart == items[b].resolvedRowStart &&
items[a].resolvedColumnStart == items[b].resolvedColumnStart );
Engine::destroySingleton();
}
UTEST( GridAutoPlacement, sparseSkipsOccupiedCells ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px 100px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Definite placement takes (1,1) and (2,2)
UIHTMLWidget* i1 = UIHTMLWidget::New();
i1->setParent( grid );
i1->setGridColumnStart( "1" );
i1->setGridRowStart( "1" );
UIHTMLWidget* i2 = UIHTMLWidget::New();
i2->setParent( grid );
i2->setGridColumnStart( "2" );
i2->setGridRowStart( "2" );
// Auto items: should fill available cells in order
UIHTMLWidget* a1 = UIHTMLWidget::New();
a1->setParent( grid );
UIHTMLWidget* a2 = UIHTMLWidget::New();
a2->setParent( grid );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 4u );
// a1: (1,1) is taken by i1, so (1,2) is first available
EXPECT_EQ( items[2].resolvedRowStart, 1 );
EXPECT_EQ( items[2].resolvedColumnStart, 2 );
// a2: sparse → continues from cursor. After a1 at (1,2), next is (1,3) or (2,1)
// Since a1 took (1,2), and (1,3) is free, a2 should go to (1,3)
EXPECT_EQ( items[3].resolvedRowStart, 1 );
EXPECT_EQ( items[3].resolvedColumnStart, 3 );
Engine::destroySingleton();
}
UTEST( GridAutoPlacement, honorsOrder ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* i1 = UIHTMLWidget::New();
i1->setParent( grid );
i1->setOrder( 1 );
UIHTMLWidget* i2 = UIHTMLWidget::New();
i2->setParent( grid );
i2->setOrder( 0 );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 2u );
// i2 has order 0, should be placed first: (1,1)
EXPECT_EQ( items[0].resolvedRowStart, 1 );
EXPECT_EQ( items[0].resolvedColumnStart, 1 );
// i1 has order 1, should be placed second: (1,2)
EXPECT_EQ( items[1].resolvedRowStart, 1 );
EXPECT_EQ( items[1].resolvedColumnStart, 2 );
Engine::destroySingleton();
}
UTEST( GridAutoPlacement, spanSkipsTooSmallHoles ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
grid->setPixelsSize( 500, 200 );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px 100px" );
UIHTMLWidget* i1 = UIHTMLWidget::New();
i1->setParent( grid );
i1->setGridColumnStart( "1" );
i1->setGridRowStart( "1" );
UIHTMLWidget* spanItem = UIHTMLWidget::New();
spanItem->setParent( grid );
spanItem->setGridColumnEnd( "span 2" );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 2u );
EXPECT_EQ( items[1].resolvedRowStart, 1 );
EXPECT_EQ( items[1].resolvedColumnStart, 2 );
EXPECT_EQ( items[1].resolvedColumnEnd, 4 );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Phase 7: Basic Track Sizing and Layout
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridLayout, fixedTracksPositionCorrectly ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 200px" );
grid->setGridTemplateRows( "50px 80px" );
grid->setPixelsSize( 500, 300 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Child 1: column 1 / span 1, row 1 / span 1 (default)
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
// Child 2: column 2 / span 1, row 1 / span 2
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
c2->setGridColumnStart( "2" );
c2->setGridRowStart( "1" );
c2->setGridRowEnd( "span 2" );
sceneNode->updateDirtyLayouts();
// c1: column 1, row 1 → x=0, y=0, w=100, h=50
EXPECT_NEAR( c1->getPixelsPosition().x, -500.f, 500.f ); // relative position within parent
// Use data from item resolution
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 2u );
// c1: column 1-2, row 1-2
EXPECT_EQ( items[0].resolvedColumnStart, 1 );
EXPECT_EQ( items[0].resolvedColumnEnd, 2 );
EXPECT_EQ( items[0].resolvedRowStart, 1 );
EXPECT_EQ( items[0].resolvedRowEnd, 2 );
// c2: column 2-3, row 1-3
EXPECT_EQ( items[1].resolvedColumnStart, 2 );
EXPECT_EQ( items[1].resolvedColumnEnd, 3 );
EXPECT_EQ( items[1].resolvedRowStart, 1 );
EXPECT_EQ( items[1].resolvedRowEnd, 3 );
Engine::destroySingleton();
}
UTEST( GridAreasParser, noneIsNone ) {
GridAreaTemplate tmpl = GridAreasParser::parseAreas( "none" );
EXPECT_TRUE( tmpl.none );
EXPECT_TRUE( tmpl.valid );
EXPECT_EQ( tmpl.areas.size(), 0u );
}
UTEST( GridAreasParser, dotNullCellsAreAccepted ) {
GridAreaTemplate tmpl = GridAreasParser::parseAreas( "\"header header\" \". content\"" );
EXPECT_TRUE( tmpl.valid );
EXPECT_TRUE( tmpl.areas.find( "header" ) != tmpl.areas.end() );
EXPECT_TRUE( tmpl.areas.find( "." ) == tmpl.areas.end() );
EXPECT_TRUE( tmpl.areas.find( "content" ) != tmpl.areas.end() );
}
UTEST( GridAreasParser, unevenRowWidthsInvalidate ) {
GridAreaTemplate tmpl = GridAreasParser::parseAreas( "\"a a\" \"a\"" );
EXPECT_FALSE( tmpl.valid );
}
UTEST( GridAreasParser, disconnectedAreaTokensInvalidate ) {
// "a . a" means area "a" is split into two separate regions
GridAreaTemplate tmpl = GridAreasParser::parseAreas( "\"a . a\"" );
EXPECT_FALSE( tmpl.valid );
}
UTEST( GridAreasParser, implicitLineNamesFromAreas ) {
GridAreaTemplate tmpl = GridAreasParser::parseAreas( "\"head head\" \"nav main\"" );
EXPECT_TRUE( tmpl.valid );
// Row line 1: head-start
std::vector<std::string> rowLine1 = GridAreasParser::getLineNamesForRowLine( tmpl, 1 );
EXPECT_EQ( rowLine1.size(), 1u );
EXPECT_TRUE( rowLine1[0] == "head-start" );
// Row line 2: head-end, nav-start, main-start
std::vector<std::string> rowLine2 = GridAreasParser::getLineNamesForRowLine( tmpl, 2 );
// head ends at row 2, nav and main start at row 2
EXPECT_EQ( rowLine2.size(), 3u );
// Column line 1: head-start, nav-start
std::vector<std::string> colLine1 = GridAreasParser::getLineNamesForColumnLine( tmpl, 1 );
EXPECT_EQ( colLine1.size(), 2u );
// Column line 3: head-end, main-end
std::vector<std::string> colLine3 = GridAreasParser::getLineNamesForColumnLine( tmpl, 3 );
EXPECT_EQ( colLine3.size(), 2u );
EXPECT_TRUE( colLine3[0] == "head-end" || colLine3[1] == "head-end" );
EXPECT_TRUE( colLine3[0] == "main-end" || colLine3[1] == "main-end" );
}
UTEST( GridParser, fixedTracks ) {
GridTrackList list = GridTrackParser::parseTrackList( "100px 1fr auto" );
EXPECT_TRUE( list.valid );
EXPECT_EQ( list.tracks.size(), 3u );
EXPECT_EQ( list.tracks[0].size.min.type, GridTrackBreadthType::Length );
EXPECT_EQ( list.tracks[0].size.min.value, 100.f );
EXPECT_EQ( list.tracks[1].size.min.type, GridTrackBreadthType::Flex );
EXPECT_EQ( list.tracks[1].size.min.value, 1.f );
EXPECT_EQ( list.tracks[2].size.min.type, GridTrackBreadthType::Auto );
}
UTEST( GridParser, lineNames ) {
GridTrackList list = GridTrackParser::parseTrackList( "[start] 1fr [mid a] 2fr [end]" );
EXPECT_TRUE( list.valid );
EXPECT_EQ( list.tracks.size(), 2u );
EXPECT_EQ( list.tracks[0].beforeLineNames.size(), 1u );
EXPECT_TRUE( list.tracks[0].beforeLineNames[0] == "start" );
EXPECT_EQ( list.tracks[1].beforeLineNames.size(), 2u );
EXPECT_TRUE( list.tracks[1].beforeLineNames[0] == "mid" );
EXPECT_TRUE( list.tracks[1].beforeLineNames[1] == "a" );
EXPECT_EQ( list.tracks[0].size.min.type, GridTrackBreadthType::Flex );
EXPECT_EQ( list.tracks[1].size.min.type, GridTrackBreadthType::Flex );
EXPECT_EQ( list.tracks[1].size.min.value, 2.f );
}
UTEST( GridParser, repeatExpands ) {
GridTrackList list = GridTrackParser::parseTrackList( "repeat(3, 10px [x] 1fr)" );
EXPECT_TRUE( list.valid );
EXPECT_EQ( list.tracks.size(), 6u );
EXPECT_EQ( list.tracks[0].beforeLineNames.size(), 0u );
EXPECT_EQ( list.tracks[0].size.min.type, GridTrackBreadthType::Length );
EXPECT_EQ( list.tracks[0].size.min.value, 10.f );
EXPECT_EQ( list.tracks[1].beforeLineNames.size(), 1u );
EXPECT_TRUE( list.tracks[1].beforeLineNames[0] == "x" );
EXPECT_EQ( list.tracks[1].size.min.type, GridTrackBreadthType::Flex );
EXPECT_EQ( list.tracks[1].size.min.value, 1.f );
}
UTEST( GridParser, invalidRepeatCount ) {
{
GridTrackList list = GridTrackParser::parseTrackList( "repeat(-1, 1fr)" );
EXPECT_FALSE( list.valid );
}
{
GridTrackList list = GridTrackParser::parseTrackList( "repeat(abc, 1fr)" );
EXPECT_FALSE( list.valid );
}
}
UTEST( GridParser, minmax ) {
GridTrackList list = GridTrackParser::parseTrackList( "minmax(100px, 1fr)" );
EXPECT_TRUE( list.valid );
EXPECT_EQ( list.tracks.size(), 1u );
EXPECT_EQ( list.tracks[0].size.min.type, GridTrackBreadthType::Length );
EXPECT_EQ( list.tracks[0].size.min.value, 100.f );
EXPECT_EQ( list.tracks[0].size.max.type, GridTrackBreadthType::Flex );
EXPECT_EQ( list.tracks[0].size.max.value, 1.f );
}
UTEST( GridParser, fitContent ) {
GridTrackList list = GridTrackParser::parseTrackList( "fit-content(200px)" );
EXPECT_TRUE( list.valid );
EXPECT_EQ( list.tracks.size(), 1u );
EXPECT_EQ( list.tracks[0].size.min.type, GridTrackBreadthType::Auto );
EXPECT_EQ( list.tracks[0].size.max.type, GridTrackBreadthType::FitContent );
EXPECT_EQ( list.tracks[0].size.max.value, 200.f );
}
UTEST( GridParser, percentageRemainsPercentage ) {
GridTrackList list = GridTrackParser::parseTrackList( "50% 100px" );
EXPECT_TRUE( list.valid );
EXPECT_EQ( list.tracks.size(), 2u );
EXPECT_EQ( list.tracks[0].size.min.type, GridTrackBreadthType::Percentage );
EXPECT_EQ( list.tracks[0].size.min.value, 50.f );
EXPECT_EQ( list.tracks[1].size.min.type, GridTrackBreadthType::Length );
EXPECT_EQ( list.tracks[1].size.min.value, 100.f );
}
// ─────────────────────────────────────────────────────────────────────────────
// Phase 0: Display routing and property defaults
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, inlineGridSetsWrapContentWidth ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setLayoutWidthPolicy( SizePolicy::MatchParent );
grid->setDisplay( CSSDisplay::InlineGrid );
EXPECT_EQ( grid->getLayoutWidthPolicy(), SizePolicy::WrapContent );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Phase 3: Grid Item Collection
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, collectsInFlowChildren ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* child1 = UIHTMLWidget::New();
child1->setParent( grid );
child1->setPixelsSize( 100, 50 );
child1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* child2 = UIHTMLWidget::New();
child2->setParent( grid );
child2->setPixelsSize( 100, 50 );
child2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Layout should not crash and children should remain valid
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( child1->getParent() == grid );
EXPECT_TRUE( child2->getParent() == grid );
Engine::destroySingleton();
}
UTEST( GridContainer, skipsOutOfFlowChildren ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* normalChild = UIHTMLWidget::New();
normalChild->setParent( grid );
normalChild->setPixelsSize( 100, 50 );
normalChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* absoluteChild = UIHTMLWidget::New();
absoluteChild->setParent( grid );
absoluteChild->setCSSPosition( CSSPosition::Absolute );
absoluteChild->setPixelsSize( 100, 50 );
absoluteChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
absoluteChild->setPixelsPosition( 0, 0 );
sceneNode->updateDirtyLayouts();
// Normal child stays; absolute child should not be disrupted by grid layout
EXPECT_TRUE( normalChild->getParent() == grid );
EXPECT_EQ( absoluteChild->getPixelsPosition().x, 0.f );
Engine::destroySingleton();
}
UTEST( GridContainer, skipsDisplayNoneChildren ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* visibleChild = UIHTMLWidget::New();
visibleChild->setParent( grid );
visibleChild->setPixelsSize( 100, 50 );
visibleChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* hiddenChild = UIHTMLWidget::New();
hiddenChild->setParent( grid );
hiddenChild->setDisplay( CSSDisplay::None );
hiddenChild->setPixelsSize( 100, 50 );
hiddenChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( visibleChild->getParent() == grid );
Engine::destroySingleton();
}
UTEST( GridContainer, ignoresWhitespaceTextNodes ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* child = UIHTMLWidget::New();
child->setParent( grid );
child->setPixelsSize( 100, 50 );
child->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Node with only whitespace should not disrupt grid layout
UITextNode* wsNode = UITextNode::New();
wsNode->setParent( grid );
wsNode->setText( " " );
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( child->getParent() == grid );
Engine::destroySingleton();
}
UTEST( GridContainer, anonymousTextNodeItem ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UITextNode* textNode = UITextNode::New();
textNode->setParent( grid );
textNode->setText( "Hello" );
// Non-whitespace text node should be collected as anonymous item (no crash)
sceneNode->updateDirtyLayouts();
Engine::destroySingleton();
}
UTEST( GridContainer, blockifiesInlineChildren ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
// Inline-block child inside a grid container should be blockified (not be inline)
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* inlineChild = UIHTMLWidget::New();
inlineChild->setParent( grid );
inlineChild->setDisplay( CSSDisplay::InlineBlock );
inlineChild->setPixelsSize( 100, 50 );
inlineChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// UILayouterManager creates a BlockLayouter for the child because parent is grid
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( inlineChild->getParent() == grid );
Engine::destroySingleton();
}
UTEST( GridContainer, floatDoesNotAffectGridItemPlacement ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* floatLeft = UIHTMLWidget::New();
floatLeft->setParent( grid );
floatLeft->setCSSFloat( CSSFloat::Left );
floatLeft->setPixelsSize( 100, 50 );
floatLeft->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* floatRight = UIHTMLWidget::New();
floatRight->setParent( grid );
floatRight->setCSSFloat( CSSFloat::Right );
floatRight->setPixelsSize( 100, 50 );
floatRight->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Floats inside grid should not cause issues
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( floatLeft->getParent() == grid );
EXPECT_TRUE( floatRight->getParent() == grid );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Auto-repeat parsing & expansion (fixed bug: propagated metadata)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridParser, autoRepeatFillsStored ) {
GridTrackList list = GridTrackParser::parseTrackList( "repeat(auto-fill, minmax(100px, 1fr))" );
EXPECT_TRUE( list.valid );
EXPECT_TRUE( list.hasAutoRepeat );
EXPECT_EQ( list.autoRepeatTemplate.size(), 1u );
EXPECT_FALSE( list.autoRepeatIsFit );
}
UTEST( GridParser, autoFitFillsStored ) {
GridTrackList list = GridTrackParser::parseTrackList( "repeat(auto-fit, 100px)" );
EXPECT_TRUE( list.valid );
EXPECT_TRUE( list.hasAutoRepeat );
EXPECT_TRUE( list.autoRepeatIsFit );
EXPECT_EQ( list.autoRepeatTemplate.size(), 1u );
EXPECT_EQ( list.autoRepeatTemplate[0].size.min.type, GridTrackBreadthType::Length );
EXPECT_EQ( list.autoRepeatTemplate[0].size.min.value, 100.f );
}
// ─────────────────────────────────────────────────────────────────────────────
// expandAutoRepeat count computation (fixed bug: used max instead of min)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridParser, expandAutoRepeatWithMinmax ) {
GridTrackList list = GridTrackParser::parseTrackList( "repeat(auto-fill, minmax(140px, 1fr))" );
EXPECT_TRUE( list.valid );
EXPECT_TRUE( list.hasAutoRepeat );
// The template track should have min=140px length, max=1fr flex
const auto& t = list.autoRepeatTemplate[0];
EXPECT_EQ( t.size.min.type, GridTrackBreadthType::Length );
EXPECT_EQ( t.size.min.value, 140.f );
EXPECT_EQ( t.size.max.type, GridTrackBreadthType::Flex );
// expandAutoRepeat should use min (140px) for count computation, not max (1fr)
GridTrackParser::expandAutoRepeat( list, 1000.f, 10.f );
EXPECT_FALSE( list.hasAutoRepeat );
// Count = floor((1000 + 10) / (140 + 10)) = floor(6.73) = 6
EXPECT_EQ( list.tracks.size(), 6u );
}
UTEST( GridParser, expandAutoRepeatZeroAvailable ) {
GridTrackList list = GridTrackParser::parseTrackList( "repeat(auto-fill, 100px)" );
// With 0 available size, should default to 1 repetition
GridTrackParser::expandAutoRepeat( list, 0.f, 10.f );
EXPECT_EQ( list.tracks.size(), 1u );
}
// ─────────────────────────────────────────────────────────────────────────────
// expandAutoRepeat insertion position (fixed bug: pushed to end)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridParser, autoRepeatPositionInsert ) {
// "100px repeat(auto-fill, 1fr) 200px" → 100px goes before repeat, 200px after
GridTrackList list = GridTrackParser::parseTrackList( "100px repeat(auto-fill, 1fr) 200px" );
EXPECT_TRUE( list.valid );
EXPECT_TRUE( list.hasAutoRepeat );
// Before expansion: tracks should be [100px, 200px], autoRepeatPosition=1
EXPECT_EQ( list.tracks.size(), 2u );
EXPECT_EQ( list.tracks[0].size.min.value, 100.f );
EXPECT_EQ( list.tracks[1].size.min.value, 200.f );
EXPECT_EQ( list.autoRepeatPosition, 1u );
GridTrackParser::expandAutoRepeat( list, 1000.f, 10.f );
// After expansion: tracks should be [100px, 1fr, 200px] (count=1 since 1fr has no min size)
EXPECT_EQ( list.tracks.size(), 3u );
EXPECT_EQ( list.tracks[0].size.min.value, 100.f );
EXPECT_EQ( list.tracks[2].size.min.value, 200.f );
EXPECT_EQ( list.tracks[1].size.min.type, GridTrackBreadthType::Flex );
}
// ─────────────────────────────────────────────────────────────────────────────
// Column count and sizing with auto-fit (fixed bug: expandAutoRepeat + collapse)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, autoFitColumnCount ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "repeat(auto-fit, minmax(140px, 1fr))" );
grid->setColumnGap( "10px" );
grid->setPixelsSize( 1000, 300 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// 5 items
for ( int i = 0; i < 5; ++i ) {
UIHTMLWidget* card = UIHTMLWidget::New();
card->setParent( grid );
card->setPixelsSize( 100, 50 );
card->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
}
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& cols = layouter->getColumns();
const auto& items = layouter->getItems();
// auto-fit should create ~6 columns but collapse down to 5 used columns
EXPECT_EQ( cols.size(), 5u );
EXPECT_EQ( items.size(), 5u );
// Each column should be (1000 - 4*10) / 5 = 192
EXPECT_NEAR( cols[0].baseSize, 192.f, 2.f );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Item width and height stretch (fixed bugs: Fixed policy + preSizeItems)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, itemWidthStretch ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "200px 300px" );
grid->setPixelsSize( 600, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
sceneNode->updateDirtyLayouts();
// c1: column 1 → width 200px
EXPECT_NEAR( c1->getPixelsSize().getWidth(), 200.f, 1.f );
// c2: column 2 → width 300px
EXPECT_NEAR( c2->getPixelsSize().getWidth(), 300.f, 1.f );
Engine::destroySingleton();
}
UTEST( GridContainer, itemHeightStretch ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "200px 300px" );
grid->setGridTemplateRows( "100px" );
grid->setPixelsSize( 600, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Two items in the same row, different content heights
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
c1->setPixelsSize( 200, 40 );
c1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
c2->setPixelsSize( 300, 60 );
c2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
sceneNode->updateDirtyLayouts();
// Both items should stretch to 100px row height
EXPECT_NEAR( c1->getPixelsSize().getHeight(), 100.f, 2.f );
EXPECT_NEAR( c2->getPixelsSize().getHeight(), 100.f, 2.f );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Gaps between items (fixed bug: cells included gaps, no visible separation)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, gapBetweenItems ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px 100px" );
grid->setColumnGap( "20px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
UIHTMLWidget* c3 = UIHTMLWidget::New();
c3->setParent( grid );
sceneNode->updateDirtyLayouts();
// c1 at x=0, width=100
EXPECT_NEAR( c1->getPixelsPosition().x, 0.f, 1.f );
EXPECT_NEAR( c1->getPixelsSize().getWidth(), 100.f, 1.f );
// c2 at x=120 (= 100 + 20 gap), width=100
EXPECT_NEAR( c2->getPixelsPosition().x, 120.f, 1.f );
EXPECT_NEAR( c2->getPixelsSize().getWidth(), 100.f, 1.f );
// c3 at x=240 (= 100 + 20 + 100 + 20), width=100
EXPECT_NEAR( c3->getPixelsPosition().x, 240.f, 1.f );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Multi-span internal gaps (fixed bug: spanning items missed internal gaps)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, spanningItemWidthIncludesGaps ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px 100px" );
grid->setColumnGap( "10px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* spanItem = UIHTMLWidget::New();
spanItem->setParent( grid );
spanItem->setGridColumnStart( "1" );
spanItem->setGridColumnEnd( "span 3" );
UIHTMLWidget* filler = UIHTMLWidget::New();
filler->setParent( grid );
sceneNode->updateDirtyLayouts();
// Span-3 item: 100 + 10 + 100 + 10 + 100 = 320
EXPECT_NEAR( spanItem->getPixelsSize().getWidth(), 320.f, 1.f );
Engine::destroySingleton();
}
UTEST( GridContainer, spanningItemTwoColumns ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 200px" );
grid->setColumnGap( "15px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* spanItem = UIHTMLWidget::New();
spanItem->setParent( grid );
spanItem->setGridColumnStart( "1" );
spanItem->setGridColumnEnd( "span 2" );
sceneNode->updateDirtyLayouts();
// Span-2 item: 100 + 15 + 200 = 315
EXPECT_NEAR( spanItem->getPixelsSize().getWidth(), 315.f, 1.f );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Justify-content alignment (fixed bug: space-between etc. just shifted grid)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, justifyContentCenter ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px" );
grid->setJustifyContent( CSSJustifyContentHelper::fromString( "center" ) );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
sceneNode->updateDirtyLayouts();
// Total content: 100 + 100 = 200 (no gaps by default)
// Extra: 500 - 200 = 300 → center at 150
EXPECT_NEAR( c1->getPixelsPosition().x, 150.f, 2.f );
EXPECT_NEAR( c2->getPixelsPosition().x, 250.f, 2.f );
Engine::destroySingleton();
}
UTEST( GridContainer, justifyContentSpaceBetween ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px 100px" );
grid->setJustifyContent( CSSJustifyContentHelper::fromString( "space-between" ) );
grid->setPixelsSize( 600, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
UIHTMLWidget* c3 = UIHTMLWidget::New();
c3->setParent( grid );
sceneNode->updateDirtyLayouts();
// c2 should be positioned farther right than c1 + its width (gap present)
EXPECT_GT( c2->getPixelsPosition().x, c1->getPixelsPosition().x + 100.f );
// c3 should be at or beyond c2 + c2's width
EXPECT_GT( c3->getPixelsPosition().x, c2->getPixelsPosition().x + 100.f );
Engine::destroySingleton();
}
UTEST( GridContainer, justifyContentSpaceAround ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px" );
grid->setJustifyContent( CSSJustifyContentHelper::fromString( "space-around" ) );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
sceneNode->updateDirtyLayouts();
// Total: 200, extra: 300, 2 items → extra/2 = 150 per gap between
// half-gap before c1 = 150/2 = 75, gap between = 150, half-gap after c2 = 75
// c1 x = 75, c2 x = 75 + 100 + 150 = 325
EXPECT_NEAR( c1->getPixelsPosition().x, 75.f, 2.f );
EXPECT_NEAR( c2->getPixelsPosition().x, 325.f, 2.f );
Engine::destroySingleton();
}
UTEST( GridContainer, justifyContentSpaceEvenly ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px" );
grid->setJustifyContent( CSSJustifyContentHelper::fromString( "space-evenly" ) );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
sceneNode->updateDirtyLayouts();
// Total: 200, extra: 300, 2 cols + 1 = 3 slots → extra/3 = 100 each
// c1 x = 100, c2 x = 100 + 100 + 100 = 300
EXPECT_NEAR( c1->getPixelsPosition().x, 100.f, 2.f );
EXPECT_NEAR( c2->getPixelsPosition().x, 300.f, 2.f );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Axis-correct gap in track sizing (fixed bug: mColumnGap used for rows too)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, rowGapWorks ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px" );
grid->setGridTemplateRows( "50px 50px" );
grid->setRowGap( "10px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Place items explicitly in different rows
UIHTMLWidget* c1 = UIHTMLWidget::New();
c1->setParent( grid );
c1->setGridColumnStart( "1" );
c1->setGridRowStart( "1" );
UIHTMLWidget* c2 = UIHTMLWidget::New();
c2->setParent( grid );
c2->setGridColumnStart( "1" );
c2->setGridRowStart( "2" );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 2u );
// Explicitly placed: item 0 at row 1, item 1 at row 2
EXPECT_EQ( items[0].resolvedRowStart, 1 );
EXPECT_EQ( items[1].resolvedRowStart, 2 );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Negative line number clamping (fixed bug: no clamp, could go ≤0)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, negativeLineClamped ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 200px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Very negative end line: grid-column: 1 / -100
UIHTMLWidget* item = UIHTMLWidget::New();
item->setParent( grid );
item->setGridColumnStart( "1" );
item->setGridColumnEnd( "-100" );
// Should not crash and should clamp to line 1 (start == end → span 1)
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_GT( items.size(), 0u );
// Item should be placed (not stuck at 0,0)
EXPECT_GT( items[0].resolvedColumnEnd, 0 );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Empty grid (fixed bug: unsigned underflow on tracks.size()-1)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, emptyGridNoCrash ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "none" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// No children — should not crash with unsigned underflow
sceneNode->updateDirtyLayouts();
EXPECT_TRUE( true ); // reached without crash
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Composite grid test: multiple columns, gaps, sizing, alignment
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, fullGridLayoutSmoke ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "repeat(3, 1fr)" );
grid->setGridTemplateRows( "auto auto" );
grid->setColumnGap( "10px" );
grid->setRowGap( "8px" );
grid->setPixelsSize( 600, 300 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
for ( int i = 0; i < 6; ++i ) {
UIHTMLWidget* child = UIHTMLWidget::New();
child->setParent( grid );
child->setPixelsSize( 50, 30 );
child->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
}
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
const auto& cols = layouter->getColumns();
EXPECT_EQ( items.size(), 6u );
EXPECT_GE( cols.size(), 3u ); // repeat(3, 1fr) should create at least 3 columns
// All items placed
for ( const auto& item : items ) {
EXPECT_GT( item.resolvedRowStart, 0 );
EXPECT_GT( item.resolvedColumnStart, 0 );
}
// No overlapping positions
for ( size_t a = 0; a < items.size(); ++a )
for ( size_t b = a + 1; b < items.size(); ++b )
EXPECT_FALSE( items[a].resolvedRowStart == items[b].resolvedRowStart &&
items[a].resolvedColumnStart == items[b].resolvedColumnStart );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// Tests for FIXED bugs — previously TODO, now passing
// ─────────────────────────────────────────────────────────────────────────────
// P1: Negative fr rejected
UTEST( GridParser, rejectNegativeFr ) {
GridTrackList list = GridTrackParser::parseTrackList( "-1fr" );
EXPECT_FALSE( list.valid );
}
// P4: Auto-repeat zero-min tracks fills space
UTEST( GridContainer, autoRepeatZeroMinFillsContainer ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "repeat(auto-fill, minmax(0, 1fr))" );
grid->setColumnGap( "10px" );
grid->setPixelsSize( 1000, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& cols = layouter->getColumns();
// Zero-min auto-fill should expand to multiple columns, not just 1
EXPECT_GT( cols.size(), 1u );
Engine::destroySingleton();
}
// PL1: Definite span on auto axis
UTEST( GridContainer, definiteColumnSpanAutoRow ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px 100px 100px" );
grid->setGridTemplateRows( "50px 50px" );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// grid-column: 2 / span 2 (should span columns 2-3), grid-row: auto
UIHTMLWidget* item1 = UIHTMLWidget::New();
item1->setParent( grid );
item1->setGridColumnStart( "2" );
item1->setGridColumnEnd( "span 2" );
item1->setPixelsSize( 50, 30 );
item1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* item2 = UIHTMLWidget::New();
item2->setParent( grid );
item2->setPixelsSize( 50, 30 );
item2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_GE( items.size(), 2u );
// Item1 should span 2 columns
EXPECT_EQ( items[0].resolvedColumnEnd - items[0].resolvedColumnStart, 2 );
// Item2 should NOT overlap with item1
EXPECT_NE( items[0].resolvedColumnStart, items[1].resolvedColumnStart );
Engine::destroySingleton();
}
// PL2: Dense auto-placement fills holes
UTEST( GridContainer, denseAutoPlacement ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px 100px" );
grid->setGridAutoFlowDense( true );
grid->setPixelsSize( 300, 300 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Place item spanning entire first row
UIHTMLWidget* big = UIHTMLWidget::New();
big->setParent( grid );
big->setGridColumnStart( "1" );
big->setGridColumnEnd( "3" ); // span both columns
big->setPixelsSize( 50, 30 );
big->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
// Small items should fill the hole in row 1 col 1? No — big occupies both cols.
// The test validates dense mode doesn't crash on sparse/empty layout.
for ( int i = 0; i < 3; ++i ) {
UIHTMLWidget* small = UIHTMLWidget::New();
small->setParent( grid );
small->setPixelsSize( 30, 20 );
small->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
}
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
EXPECT_EQ( items.size(), 4u );
Engine::destroySingleton();
}
// TS4: Flex distribution clamps to growth limit
UTEST( GridContainer, flexGrowthLimit ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
// minmax(0, 100px) with 1fr — should be clamped to at most 100px
grid->setGridTemplateColumns( "minmax(0, 100px) 1fr" );
grid->setPixelsSize( 400, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& cols = layouter->getColumns();
ASSERT_GE( cols.size(), 2u );
// First column max is 100px even though it gets flex space
EXPECT_LE( cols[0].baseSize, 100.f + 1.f );
Engine::destroySingleton();
}
// TS7: grid-auto-rows/columns
UTEST( GridContainer, implicitTrackSizing ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "100px" );
grid->setGridAutoRows( "80px" );
grid->setPixelsSize( 500, 300 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
for ( int i = 0; i < 3; ++i ) {
UIHTMLWidget* item = UIHTMLWidget::New();
item->setParent( grid );
item->setPixelsSize( 50, 30 );
item->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
}
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& rows = layouter->getColumns(); // intentional: getColumns for rows check
// Items placed in separate rows with 80px auto row height
(void)rows; // no crash
Engine::destroySingleton();
}
// I1: computeIntrinsicWidths with auto-repeat
UTEST( GridContainer, intrinsicWidthAutoRepeat ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateColumns( "repeat(auto-fill, 100px)" );
grid->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
Float minW = layouter->getMinIntrinsicWidth();
Float maxW = layouter->getMaxIntrinsicWidth();
// Auto-repeat should produce non-zero intrinsic widths
EXPECT_GT( minW, 0.f );
// Both min and max should be > 0 (fixed-length tracks have equal min/max)
EXPECT_GT( maxW, 0.f );
Engine::destroySingleton();
}
// ─────────────────────────────────────────────────────────────────────────────
// HTML fixture tests (moved from uihtml_tests.cpp)
// ─────────────────────────────────────────────────────────────────────────────
UTEST( GridContainer, basicHTMLGrid ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "UIHTML Grid Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings() );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
sceneNode->loadLayoutFromString(
HTMLFormatter::HTMLtoXML( "<div style=\"display: grid; grid-template-columns: 100px 200px; "
"grid-template-rows: 50px 80px; width: 400px; height: 200px;\">"
"<div>A</div>"
"<div style=\"grid-column: 2; grid-row: 1/3;\">B</div>"
"</div>" ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto gridEl = sceneNode->getRoot()->findByTag( "div" );
ASSERT_TRUE( nullptr != gridEl );
EXPECT_TRUE( gridEl->isType( UI_TYPE_HTML_WIDGET ) );
Engine::destroySingleton();
}
UTEST( GridContainer, downloadGridLayout ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "UIHTML Grid Download Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings() );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML(
"<!doctype html><html><body>"
"<div class=\"download-grid\" style=\"display: grid; "
"grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px;\">"
"<div class=\"download-card\"><h3>Windows</h3></div>"
"<div class=\"download-card\"><h3>Mac</h3></div>"
"<div class=\"download-card\"><h3>Linux</h3></div>"
"<div class=\"download-card\"><h3>Android</h3></div>"
"<div class=\"download-card\"><h3>iOS</h3></div>"
"</div></body></html>" ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto gridEl = sceneNode->getRoot()->findByClass( "download-grid" );
ASSERT_TRUE( nullptr != gridEl );
ASSERT_TRUE( gridEl->isType( UI_TYPE_HTML_WIDGET ) );
UIHTMLWidget* gridWidget = gridEl->asType<UIHTMLWidget>();
auto* layouter = gridWidget->getLayouter();
ASSERT_TRUE( nullptr != layouter );
auto* gridLayouter = static_cast<GridLayouter*>( layouter );
const auto& cols = gridLayouter->getColumns();
EXPECT_TRUE( cols.size() >= 2 );
const auto& items = gridLayouter->getItems();
ASSERT_EQ( (size_t)5, items.size() );
Float gridW = gridWidget->getPixelsSize().getWidth();
Float expectedCardW = ( gridW - 4.f * 10.f ) / 5.f;
Float tolerance = expectedCardW * 0.15f;
{
int col = 1;
for ( const auto& item : items ) {
Float cardW = item.widget->getPixelsSize().getWidth();
EXPECT_NEAR( expectedCardW, cardW, tolerance );
EXPECT_EQ( 1, item.resolvedRowStart );
EXPECT_EQ( col, item.resolvedColumnStart );
++col;
}
}
{
Float firstH = items[0].widget->getPixelsSize().getHeight();
EXPECT_TRUE( firstH > 0 );
for ( size_t i = 1; i < items.size(); ++i )
EXPECT_NEAR( firstH, items[i].widget->getPixelsSize().getHeight(), 0.5f );
}
Engine::destroySingleton();
}
UTEST( GridContainer, gridTestFixturePlacesSpanningItems ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "UIHTML Grid Fixture Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings() );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
std::string htmlContent;
ASSERT_TRUE( FileSystem::fileGet( "assets/html/grid_test.html", htmlContent ) );
sceneNode->loadLayoutFromString( EE::UI::Tools::HTMLFormatter::HTMLtoXML( htmlContent ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto* wrapperNode = sceneNode->getRoot()->findByClass( "wrapper" );
ASSERT_TRUE( nullptr != wrapperNode );
ASSERT_TRUE( wrapperNode->isWidget() );
auto* one = sceneNode->getRoot()->findByClass( "one" );
auto* two = sceneNode->getRoot()->findByClass( "two" );
auto* three = sceneNode->getRoot()->findByClass( "three" );
auto* four = sceneNode->getRoot()->findByClass( "four" );
auto* five = sceneNode->getRoot()->findByClass( "five" );
auto* six = sceneNode->getRoot()->findByClass( "six" );
ASSERT_TRUE( nullptr != one );
ASSERT_TRUE( nullptr != two );
ASSERT_TRUE( nullptr != three );
ASSERT_TRUE( nullptr != four );
ASSERT_TRUE( nullptr != five );
ASSERT_TRUE( nullptr != six );
ASSERT_TRUE( one->isWidget() );
ASSERT_TRUE( two->isWidget() );
ASSERT_TRUE( three->isWidget() );
ASSERT_TRUE( four->isWidget() );
ASSERT_TRUE( five->isWidget() );
ASSERT_TRUE( six->isWidget() );
auto* oneWidget = one->asType<UIWidget>();
auto* twoWidget = two->asType<UIWidget>();
auto* threeWidget = three->asType<UIWidget>();
auto* fourWidget = four->asType<UIWidget>();
auto* fiveWidget = five->asType<UIWidget>();
auto* sixWidget = six->asType<UIWidget>();
Float wrapperW = wrapperNode->asType<UIWidget>()->getPixelsSize().getWidth();
EXPECT_GT( wrapperW, 900.f );
EXPECT_LT( wrapperW, 950.f );
EXPECT_GT( oneWidget->getPixelsSize().getWidth(), 600.f );
EXPECT_GT( twoWidget->getPixelsSize().getWidth(), 600.f );
EXPECT_GT( threeWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( fourWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( fiveWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( sixWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( oneWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_GT( twoWidget->getPixelsSize().getHeight(), 200.f );
EXPECT_GT( threeWidget->getPixelsSize().getHeight(), 300.f );
EXPECT_GT( fourWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_GT( fiveWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_GT( sixWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_NEAR( oneWidget->getPixelsPosition().y, twoWidget->getPixelsPosition().y, 1.f );
EXPECT_GT( twoWidget->getPixelsPosition().x, oneWidget->getPixelsPosition().x + 300.f );
EXPECT_GT( threeWidget->getPixelsPosition().y, oneWidget->getPixelsPosition().y + 95.f );
EXPECT_GT( fourWidget->getPixelsPosition().y, twoWidget->getPixelsPosition().y + 200.f );
EXPECT_NEAR( fiveWidget->getPixelsPosition().y, sixWidget->getPixelsPosition().y, 1.f );
EXPECT_GT( sixWidget->getPixelsPosition().x, fiveWidget->getPixelsPosition().x + 300.f );
Engine::destroySingleton();
}
UTEST( GridContainer, newsblurReducedGrid ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "UIHTML Grid NewsBlur Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings() );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
std::string htmlContent;
ASSERT_TRUE( FileSystem::fileGet( "assets/html/grid_newsblur_reduced.html", htmlContent ) );
sceneNode->loadLayoutFromString( EE::UI::Tools::HTMLFormatter::HTMLtoXML( htmlContent ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto* gridEl = sceneNode->getRoot()->findByClass( "NB-inner" );
ASSERT_TRUE( nullptr != gridEl );
ASSERT_TRUE( gridEl->isWidget() );
Float gridW = gridEl->getPixelsSize().getWidth();
Float gridH = gridEl->getPixelsSize().getHeight();
EXPECT_GT( gridW, 100.f );
EXPECT_GT( gridH, 100.f );
// Verify NB-feature-group items aren't overexpanded
auto* fg = sceneNode->getRoot()->findByClass( "NB-feature-group" );
ASSERT_TRUE( nullptr != fg );
Float fgH = fg->getPixelsSize().getHeight();
EXPECT_GT( fgH, 100.f );
EXPECT_LT( fgH, 2000.f );
auto* fgiText = sceneNode->getRoot()->findByClass( "NB-fgi-text" );
ASSERT_TRUE( nullptr != fgiText );
Float textW = fgiText->getPixelsSize().getWidth();
EXPECT_GT( textW, 150.f );
Engine::destroySingleton();
}