#include "utest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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( 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( 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( 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( 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( 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( 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( 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 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 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 colLine1 = GridAreasParser::getLineNamesForColumnLine( tmpl, 1 ); EXPECT_EQ( colLine1.size(), 2u ); // Column line 3: head-end, main-end std::vector 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( "
" "
A
" "
B
" "
" ) ); 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( "" "
" "

Windows

" "

Mac

" "

Linux

" "

Android

" "

iOS

" "
" ) ); 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(); auto* layouter = gridWidget->getLayouter(); ASSERT_TRUE( nullptr != layouter ); auto* gridLayouter = static_cast( 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(); auto* twoWidget = two->asType(); auto* threeWidget = three->asType(); auto* fourWidget = four->asType(); auto* fiveWidget = five->asType(); auto* sixWidget = six->asType(); Float wrapperW = wrapperNode->asType()->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(); }