#include "compareimages.hpp" #include "eepp/ui/uiwindow.hpp" #include "utest.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace EE; using namespace EE::Graphics; using namespace EE::Window; using namespace EE::Scene; using namespace EE::UI; using namespace EE::UI::Tools; static void init_ui_test() { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); FontFamily::loadFromRegular( font ); FontTrueType* monospace = FontTrueType::New( "monospace" ); monospace->loadFromFile( "../assets/fonts/DejaVuSansMono.ttf" ); FontFamily::loadFromRegular( monospace ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); sceneNode->setColorSchemePreference( ColorSchemePreference::Light ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); } static String uiHtmlRenderedText( const RichText& richText ) { String text; const auto& lines = richText.getLines(); for ( size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex ) { if ( lineIndex > 0 ) text += '\n'; for ( const auto& span : lines[lineIndex].spans ) { if ( span.type == RichText::RenderSpan::Type::Text && span.text ) text += span.text->getString(); } } return text; } static String uiHtmlLineText( const RichText& richText, size_t lineIndex ) { String text; const auto& lines = richText.getLines(); if ( lineIndex >= lines.size() ) return text; for ( const auto& span : lines[lineIndex].spans ) { if ( span.type == RichText::RenderSpan::Type::Text && span.text ) text += span.text->getString(); } while ( !text.empty() && ( text.front() == '\n' || text.front() == '\r' ) ) text = text.substr( 1 ); while ( !text.empty() && ( text.back() == '\n' || text.back() == '\r' ) ) text.pop_back(); return text; } UTEST( UIHTMLTable, complexLayout ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/hn_thread_test.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); /* while ( win->isRunning() ) */ { win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); } auto hnMain = sceneNode->getRoot()->find( "hnmain" ); auto bigbox = sceneNode->getRoot()->find( "bigbox" ); auto commentTree = sceneNode->getRoot()->findByClass( "comment-tree" ); auto votelinks = sceneNode->getRoot()->findByClass( "votelinks" ); auto commentTd = sceneNode->getRoot()->findByClass( "default" ); auto comment = sceneNode->getRoot()->findByClass( "comment" ); auto commtext = sceneNode->getRoot()->findByClass( "commtext" ); EXPECT_GT( commentTree->getPixelsSize().getWidth(), 0 ); EXPECT_GT( commentTree->getPixelsSize().getHeight(), 0 ); EXPECT_GT( comment->getPixelsSize().getWidth(), 0 ); EXPECT_GT( commtext->getPixelsSize().getWidth(), 0 ); EXPECT_GT( commentTd->getPixelsSize().getWidth(), 0 ); EXPECT_GT( commentTd->getPixelsSize().getHeight(), 0 ); EXPECT_GE( hnMain->getPixelsSize().getHeight(), bigbox->getPixelsSize().getHeight() ); Float totalTds = commentTd->getPixelsSize().getWidth() + votelinks->getPixelsSize().getWidth(); Float mainTotal = hnMain->getPixelsSize().getWidth(); EXPECT_GT( totalTds, 0 ); EXPECT_GT( mainTotal, 0 ); EXPECT_NEAR( totalTds, mainTotal, 0.1 ); compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout", "html" ); Engine::destroySingleton(); } UTEST( UIHTMLTable, complexLayout2 ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test 2", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/hn_threaded_test.html", html ); // Keep this fixture close to the captured viewport. Apple Software Renderer drops the // large offscreen solid-background quad if the original full HN thread is used, even // though GPU renderers and other software renderers rasterize it correctly. sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); const Color expectedMainBg( "#f6f6ef" ); UIWidget* hnMain = nullptr; for ( int i = 0; i < 8; i++ ) { win->getInput()->update(); SceneManager::instance()->update(); if ( !hnMain ) hnMain = sceneNode->getRoot()->find( "hnmain" )->asType(); win->clear(); SceneManager::instance()->draw(); win->display(); if ( hnMain && hnMain->getBackgroundColor() == expectedMainBg ) break; } ASSERT_TRUE( hnMain != nullptr ); EXPECT_STDSTREQ( hnMain->getBackgroundColor().toHexString(), expectedMainBg.toHexString() ); compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout-2", "html" ); Engine::destroySingleton(); } UTEST( UIHTML, redditOldThreadWebViewSmoke ) { if ( std::getenv( "EEPP_REDDIT_OLD_THREAD_VISUAL" ) == nullptr ) UTEST_SKIP( "set EEPP_REDDIT_OLD_THREAD_VISUAL=1 to render the old Reddit fixture" ); auto win = Engine::instance()->createWindow( WindowSettings( 1024, 1000, "Old Reddit Thread Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); if ( !FileSystem::fileExists( "assets/html/reddit_old_thread_files/reddit.ETA_etA2z5U.css" ) ) { Engine::destroySingleton(); UTEST_SKIP( "old Reddit fixture CSS asset is not readable" ); } FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); UIWebView* webView = UIWebView::New(); webView->setParent( sceneNode->getRoot() ); webView->setPixelsSize( win->getWidth(), win->getHeight() ); webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); webView->loadURI( URI( "file://" + Sys::getProcessPath() + "assets/html/reddit_old_thread.html" ) ); win->setClearColor( Color::White ); for ( int i = 0; i < 8; i++ ) { win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); } auto side = sceneNode->getRoot()->findByClass( "side" ); auto siteTable = sceneNode->getRoot()->find( "siteTable" ); auto midcol = sceneNode->getRoot()->findByClass( "midcol" ); auto entry = sceneNode->getRoot()->findByClass( "entry" ); auto arrow = sceneNode->getRoot()->findByClass( "arrow" ); auto srHeader = sceneNode->getRoot()->find( "sr-header-area" ); auto redesignButton = sceneNode->getRoot()->find( "redesign-beta-optin-btn" ); auto srDrop = sceneNode->getRoot()->querySelector( "#sr-header-area .dropdown.srdrop" ); auto srList = sceneNode->getRoot()->querySelector( "#sr-header-area .sr-list" ); auto srFlatList = sceneNode->getRoot()->querySelector( "#sr-header-area .sr-list .flat-list" ); auto headerBottomLeft = sceneNode->getRoot()->find( "header-bottom-left" ); auto headerBottomRight = sceneNode->getRoot()->find( "header-bottom-right" ); auto header = sceneNode->getRoot()->find( "header" ); auto dropChoices = sceneNode->getRoot()->querySelector( ".drop-choices.srdrop" ); auto selftextMd = sceneNode->getRoot()->querySelector( ".link .usertext-body .md" ); auto selftextFirstP = sceneNode->getRoot()->querySelector( ".link .usertext-body .md p" ); auto postTitle = sceneNode->getRoot()->querySelector( ".link .top-matter > p.title" ); auto postTagline = sceneNode->getRoot()->querySelector( ".link .top-matter > p.tagline" ); auto postExpando = sceneNode->getRoot()->querySelector( ".link .expando" ); auto postUsertext = sceneNode->getRoot()->querySelector( ".link .expando > form.usertext" ); auto commentArea = sceneNode->getRoot()->querySelector( ".commentarea" ); auto commentForm = sceneNode->getRoot()->find( "form-t3_1tk9dgh5ov" ); auto commentEdit = sceneNode->getRoot()->querySelector( ".commentarea .usertext-edit" ); auto commentTextarea = sceneNode->getRoot()->querySelector( ".commentarea textarea" ); auto commentHelpToggle = sceneNode->getRoot()->querySelector( ".commentarea .help-toggle" ); auto commentContentPolicy = sceneNode->getRoot()->querySelector( ".commentarea a.reddiquette" ); auto flairCheckbox = sceneNode->getRoot()->find( "flair_enabled" ); ASSERT_TRUE( side != nullptr ); ASSERT_TRUE( siteTable != nullptr ); ASSERT_TRUE( midcol != nullptr ); ASSERT_TRUE( entry != nullptr ); ASSERT_TRUE( arrow != nullptr ); ASSERT_TRUE( srHeader != nullptr ); ASSERT_TRUE( redesignButton != nullptr ); ASSERT_TRUE( srDrop != nullptr ); ASSERT_TRUE( srList != nullptr ); ASSERT_TRUE( srFlatList != nullptr ); ASSERT_TRUE( headerBottomLeft != nullptr ); ASSERT_TRUE( headerBottomRight != nullptr ); ASSERT_TRUE( header != nullptr ); ASSERT_TRUE( dropChoices != nullptr ); ASSERT_TRUE( selftextMd != nullptr ); ASSERT_TRUE( selftextFirstP != nullptr ); ASSERT_TRUE( postTitle != nullptr ); ASSERT_TRUE( postTagline != nullptr ); ASSERT_TRUE( postExpando != nullptr ); ASSERT_TRUE( postUsertext != nullptr ); ASSERT_TRUE( commentArea != nullptr ); ASSERT_TRUE( commentForm != nullptr ); ASSERT_TRUE( commentEdit != nullptr ); ASSERT_TRUE( commentTextarea != nullptr ); ASSERT_TRUE( commentHelpToggle != nullptr ); ASSERT_TRUE( commentContentPolicy != nullptr ); ASSERT_TRUE( flairCheckbox != nullptr ); UIWidget* content = siteTable->getParent()->isWidget() ? siteTable->getParent()->asType() : nullptr; ASSERT_TRUE( content != nullptr ); Vector2f sidePos = side->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f contentPos = content->convertToWorldSpace( { 0, 0 } ); Vector2f midcolPos = midcol->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f entryPos = entry->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f arrowPos = arrow->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f srHeaderPos = srHeader->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f redesignButtonPos = redesignButton->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f srDropPos = srDrop->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f srListPos = srList->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f srFlatListPos = srFlatList->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f headerBottomLeftPos = headerBottomLeft->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f headerBottomRightPos = headerBottomRight->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f headerPos = header->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f dropChoicesPos = dropChoices->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f selftextMdPos = selftextMd->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f selftextFirstPPos = selftextFirstP->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f postTitlePos = postTitle->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f postTaglinePos = postTagline->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f postExpandoPos = postExpando->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f postUsertextPos = postUsertext->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f commentAreaPos = commentArea->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f commentFormPos = commentForm->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f commentEditPos = commentEdit->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f commentTextareaPos = commentTextarea->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f commentHelpTogglePos = commentHelpToggle->asType()->convertToWorldSpace( { 0, 0 } ); Vector2f commentContentPolicyPos = commentContentPolicy->asType()->convertToWorldSpace( { 0, 0 } ); auto fontSizeOf = []( Node* node ) -> Uint32 { return node->isType( UI_TYPE_RICHTEXT ) ? node->asType()->getFontSize() : 0; }; std::cerr << "old reddit rects: " << "side=(" << sidePos.x << "," << sidePos.y << " " << side->asType()->getPixelsSize().getWidth() << "x" << side->asType()->getPixelsSize().getHeight() << ") " << "content=(" << contentPos.x << "," << contentPos.y << " " << content->getPixelsSize().getWidth() << "x" << content->getPixelsSize().getHeight() << ") " << "midcol=(" << midcolPos.x << "," << midcolPos.y << " " << midcol->asType()->getPixelsSize().getWidth() << "x" << midcol->asType()->getPixelsSize().getHeight() << ") " << "entry=(" << entryPos.x << "," << entryPos.y << " " << entry->asType()->getPixelsSize().getWidth() << "x" << entry->asType()->getPixelsSize().getHeight() << ") " << "arrow=(" << arrowPos.x << "," << arrowPos.y << " " << arrow->asType()->getPixelsSize().getWidth() << "x" << arrow->asType()->getPixelsSize().getHeight() << ") " << "srHeader=(" << srHeaderPos.x << "," << srHeaderPos.y << " " << srHeader->asType()->getPixelsSize().getWidth() << "x" << srHeader->asType()->getPixelsSize().getHeight() << ") " << "redesignButton=(" << redesignButtonPos.x << "," << redesignButtonPos.y << " " << redesignButton->asType()->getPixelsSize().getWidth() << "x" << redesignButton->asType()->getPixelsSize().getHeight() << ") " << "srDrop=(" << srDropPos.x << "," << srDropPos.y << " " << srDrop->asType()->getPixelsSize().getWidth() << "x" << srDrop->asType()->getPixelsSize().getHeight() << " float=" << CSSFloatHelper::toString( srDrop->asType()->getCSSFloat() ) << ") " << "srList=(" << srListPos.x << "," << srListPos.y << " " << srList->asType()->getPixelsSize().getWidth() << "x" << srList->asType()->getPixelsSize().getHeight() << " lines=" << srList->asType()->getRichTextPtr()->getLines().size() << " wrap=" << srList->asType()->getLineWrap() << ") " << "headerBottomLeft=(" << headerBottomLeftPos.x << "," << headerBottomLeftPos.y << " " << headerBottomLeft->asType()->getPixelsSize().getWidth() << "x" << headerBottomLeft->asType()->getPixelsSize().getHeight() << ") " << "headerBottomRight=(" << headerBottomRightPos.x << "," << headerBottomRightPos.y << " " << headerBottomRight->asType()->getPixelsSize().getWidth() << "x" << headerBottomRight->asType()->getPixelsSize().getHeight() << " position=" << CSSPositionHelper::toString( headerBottomRight->asType()->getCSSPosition() ) << ") " << "header=(" << headerPos.x << "," << headerPos.y << " " << header->asType()->getPixelsSize().getWidth() << "x" << header->asType()->getPixelsSize().getHeight() << " offset=" << header->asType()->getPixelsContentOffset().Left << "," << header->asType()->getPixelsContentOffset().Top << "," << header->asType()->getPixelsContentOffset().Right << "," << header->asType()->getPixelsContentOffset().Bottom << ") " << "dropChoices=(" << dropChoicesPos.x << "," << dropChoicesPos.y << " " << dropChoices->asType()->getPixelsSize().getWidth() << "x" << dropChoices->asType()->getPixelsSize().getHeight() << " visible=" << dropChoices->asType()->isVisible() << " display=" << CSSDisplayHelper::toString( dropChoices->asType()->getDisplay() ) << ") " << "selftextMd=(" << selftextMdPos.x << "," << selftextMdPos.y << " " << selftextMd->asType()->getPixelsSize().getWidth() << "x" << selftextMd->asType()->getPixelsSize().getHeight() << " font=" << fontSizeOf( selftextMd ) << ") " << "selftextFirstP=(" << selftextFirstPPos.x << "," << selftextFirstPPos.y << " " << selftextFirstP->asType()->getPixelsSize().getWidth() << "x" << selftextFirstP->asType()->getPixelsSize().getHeight() << " font=" << fontSizeOf( selftextFirstP ) << ") " << "postTitle=(" << postTitlePos.x << "," << postTitlePos.y << " " << postTitle->asType()->getPixelsSize().getWidth() << "x" << postTitle->asType()->getPixelsSize().getHeight() << " font=" << fontSizeOf( postTitle ) << ") " << "postTagline=(" << postTaglinePos.x << "," << postTaglinePos.y << " " << postTagline->asType()->getPixelsSize().getWidth() << "x" << postTagline->asType()->getPixelsSize().getHeight() << " font=" << fontSizeOf( postTagline ) << ") " << "postExpando=(" << postExpandoPos.x << "," << postExpandoPos.y << " " << postExpando->asType()->getPixelsSize().getWidth() << "x" << postExpando->asType()->getPixelsSize().getHeight() << ") " << "postUsertext=(" << postUsertextPos.x << "," << postUsertextPos.y << " " << postUsertext->asType()->getPixelsSize().getWidth() << "x" << postUsertext->asType()->getPixelsSize().getHeight() << ") " << "commentArea=(" << commentAreaPos.x << "," << commentAreaPos.y << " " << commentArea->asType()->getPixelsSize().getWidth() << "x" << commentArea->asType()->getPixelsSize().getHeight() << ") " << "commentForm=(" << commentFormPos.x << "," << commentFormPos.y << " " << commentForm->asType()->getPixelsSize().getWidth() << "x" << commentForm->asType()->getPixelsSize().getHeight() << ") " << "commentEdit=(" << commentEditPos.x << "," << commentEditPos.y << " " << commentEdit->asType()->getPixelsSize().getWidth() << "x" << commentEdit->asType()->getPixelsSize().getHeight() << ") " << "commentTextarea=(" << commentTextareaPos.x << "," << commentTextareaPos.y << " " << commentTextarea->asType()->getPixelsSize().getWidth() << "x" << commentTextarea->asType()->getPixelsSize().getHeight() << ") " << "commentContentPolicy=(" << commentContentPolicyPos.x << "," << commentContentPolicyPos.y << " " << commentContentPolicy->asType()->getPixelsSize().getWidth() << "x" << commentContentPolicy->asType()->getPixelsSize().getHeight() << ") " << "commentHelpToggle=(" << commentHelpTogglePos.x << "," << commentHelpTogglePos.y << " " << commentHelpToggle->asType()->getPixelsSize().getWidth() << "x" << commentHelpToggle->asType()->getPixelsSize().getHeight() << ")" << std::endl; const Float midcolCenter = midcolPos.x + midcol->asType()->getPixelsSize().getWidth() / 2.f; const Float arrowCenter = arrowPos.x + arrow->asType()->getPixelsSize().getWidth() / 2.f; EXPECT_NEAR( midcolCenter, arrowCenter, 1.f ); EXPECT_NEAR( srListPos.y, srHeaderPos.y, 1.f ); EXPECT_GE( srListPos.x, srDropPos.x + srDrop->asType()->getPixelsSize().getWidth() - 1.f ); EXPECT_LE( srList->asType()->getPixelsSize().getWidth(), win->getWidth() - srListPos.x + 1.f ); EXPECT_LE( srList->asType()->getPixelsSize().getHeight(), srHeader->asType()->getPixelsSize().getHeight() + 1.f ); EXPECT_EQ( srFlatList->asType()->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_NEAR( srFlatListPos.y, srHeaderPos.y, 1.f ); EXPECT_GE( headerBottomLeftPos.y, srHeaderPos.y + srHeader->asType()->getPixelsSize().getHeight() - 1.f ); EXPECT_EQ( headerBottomRight->asType()->getCSSPosition(), CSSPosition::Absolute ); EXPECT_NEAR( headerBottomRightPos.x + headerBottomRight->asType()->getPixelsSize().getWidth(), headerPos.x + header->asType()->getPixelsSize().getWidth(), 1.f ); EXPECT_NEAR( headerBottomRightPos.y + headerBottomRight->asType()->getPixelsSize().getHeight(), headerPos.y + header->asType()->getPixelsSize().getHeight() - header->asType()->getPixelsContentOffset().Bottom, 1.f ); EXPECT_FALSE( dropChoices->asType()->isVisible() ); EXPECT_EQ( dropChoices->asType()->getDisplay(), CSSDisplay::None ); ASSERT_TRUE( flairCheckbox->isType( UI_TYPE_HTML_INPUT ) ); ASSERT_TRUE( flairCheckbox->asType()->getChildWidget() != nullptr ); EXPECT_TRUE( flairCheckbox->asType()->getChildWidget()->asType()->isChecked() ); EXPECT_NEAR( commentHelpTogglePos.x + commentHelpToggle->asType()->getPixelsSize().getWidth(), commentEditPos.x + commentEdit->asType()->getPixelsSize().getWidth(), 1.f ); EXPECT_LE( commentContentPolicyPos.x + commentContentPolicy->asType()->getPixelsSize().getWidth(), commentHelpTogglePos.x - commentHelpToggle->asType()->getLayoutPixelsMargin().Left + 1.f ); auto* arrowBackground = arrow->asType()->getBackground(); ASSERT_TRUE( arrowBackground != nullptr ); auto* arrowBackgroundLayer = arrowBackground->getLayer( 0 ); ASSERT_TRUE( arrowBackgroundLayer != nullptr ); ASSERT_TRUE( arrowBackgroundLayer->getDrawable() != nullptr ); EXPECT_EQ( arrowBackgroundLayer->getDrawable()->getPixelsSize().getWidth(), 140 ); EXPECT_EQ( arrowBackgroundLayer->getDrawable()->getPixelsSize().getHeight(), 1751 ); EXPECT_STDSTREQ( arrowBackgroundLayer->getPositionX(), "-42px" ); EXPECT_STDSTREQ( arrowBackgroundLayer->getPositionY(), "-1678px" ); EXPECT_NEAR( arrowBackgroundLayer->getOffset().x, -42.f, 0.1f ); EXPECT_NEAR( arrowBackgroundLayer->getOffset().y, -1678.f, 0.1f ); if ( !FileSystem::fileExists( "output" ) ) FileSystem::makeDir( "output" ); win->getFrontBufferImage().saveToFile( "output/eepp-reddit-old-thread-current.webp", Image::SaveType::WEBP ); Engine::destroySingleton(); } UTEST( UIHTML, StrikeElementUsesDefaultLineThrough ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(

old

)html" ) ); SceneManager::instance()->update(); auto* strike = sceneNode->getRoot()->find( "strike" )->asType(); ASSERT_TRUE( strike != nullptr ); EXPECT_TRUE( 0 != ( strike->getTextDecoration() & Text::StrikeThrough ) ); Engine::destroySingleton(); } UTEST( UIHTML, FontSizeAbsoluteKeywordsUseBrowserScale ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
x-small
small
medium
large
smaller larger
)html" ) ); SceneManager::instance()->update(); auto fontSize = [sceneNode]( const std::string& id ) { auto* node = sceneNode->getRoot()->find( id ); return node->isType( UI_TYPE_TEXTSPAN ) ? node->asType()->getFontSize() : node->asType()->getFontSize(); }; EXPECT_EQ( fontSize( "xsmall" ), 10u ); EXPECT_EQ( fontSize( "small" ), 13u ); EXPECT_EQ( fontSize( "medium" ), 16u ); EXPECT_EQ( fontSize( "large" ), 18u ); EXPECT_EQ( fontSize( "smaller" ), 17u ); EXPECT_EQ( fontSize( "larger" ), 24u ); Engine::destroySingleton(); } UTEST( UIHTML, WhiteSpaceNowrapDisablesSoftWrap ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); const std::string text = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( String::format( R"html(
%s
%s
)html", text.c_str(), text.c_str() ) ) ); SceneManager::instance()->update(); auto* wrap = sceneNode->getRoot()->find( "wrap" )->asType(); auto* nowrap = sceneNode->getRoot()->find( "nowrap" )->asType(); ASSERT_TRUE( wrap != nullptr ); ASSERT_TRUE( nowrap != nullptr ); EXPECT_GT( wrap->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_EQ( nowrap->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_FALSE( nowrap->getLineWrap() ); Engine::destroySingleton(); } UTEST( UIHTML, WhiteSpaceNowrapKeepsInlineListOnOneLine ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
  • alpha
  • beta
  • gamma
  • delta
  • epsilon
  • zeta
  • eta
  • theta
)html" ) ); SceneManager::instance()->update(); auto* bar = sceneNode->getRoot()->find( "bar" )->asType(); auto* list = sceneNode->getRoot()->find( "list" )->asType(); ASSERT_TRUE( bar != nullptr ); ASSERT_TRUE( list != nullptr ); EXPECT_EQ( bar->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_FALSE( bar->getLineWrap() ); EXPECT_EQ( list->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_FALSE( list->getLineWrap() ); Engine::destroySingleton(); } UTEST( UIHTML, WhiteSpaceNowrapContinuesInlineContentAfterOverflow ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
  • home
  • - popular
  • - all
  • - friends
|
  • movies
  • - videos
  • - pcgaming
  • - gamedev
  • - science
  • - space
)html" ) ); SceneManager::instance()->update(); auto* bar = sceneNode->getRoot()->find( "bar" )->asType(); auto* first = sceneNode->getRoot()->find( "first" )->asType(); auto* second = sceneNode->getRoot()->find( "second" )->asType(); ASSERT_TRUE( bar != nullptr ); ASSERT_TRUE( first != nullptr ); ASSERT_TRUE( second != nullptr ); EXPECT_FALSE( bar->getLineWrap() ); EXPECT_FALSE( first->getLineWrap() ); EXPECT_FALSE( second->getLineWrap() ); EXPECT_EQ( bar->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_EQ( first->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_EQ( second->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_NEAR( first->convertToWorldSpace( { 0, 0 } ).y, second->convertToWorldSpace( { 0, 0 } ).y, 1.f ); Engine::destroySingleton(); } UTEST( UIHTML, WhiteSpaceCollapsePreCodePreservesIndentation ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
if (x) {
    return 1;
}
)html" ) ); SceneManager::instance()->update(); auto* pre = sceneNode->getRoot()->find( "pre" )->asType(); auto* code = sceneNode->getRoot()->find( "code" ); ASSERT_TRUE( pre != nullptr ); ASSERT_TRUE( code != nullptr ); EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) ); EXPECT_GE( pre->getRichTextPtr()->getLines().size(), (size_t)3 ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "if (x) {" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), " return 1;" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "}" ); Engine::destroySingleton(); } UTEST( UIHTML, PreCodeSimpleFixtureKeepsCompactCodeLines ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre.code.html", html ) ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); SceneManager::instance()->update(); UIRichText* pre = nullptr; Node* code = nullptr; sceneNode->getRoot()->forEachNode( [&pre, &code]( Node* node ) { if ( node->isWidget() && node->asType()->getElementTag() == "pre" ) pre = node->asType(); if ( node->isWidget() && node->asType()->getElementTag() == "code" ) code = node; } ); ASSERT_TRUE( pre != nullptr ); ASSERT_TRUE( code != nullptr ); EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) ); auto* codeSpan = code->asType(); ASSERT_TRUE( codeSpan != nullptr ); const auto& lines = pre->getRichTextPtr()->getLines(); ASSERT_GE( lines.size(), (size_t)3 ); ASSERT_LE( lines.size(), (size_t)4 ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "void main() {" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), " printf(\"Hello World\");" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "}" ); if ( lines.size() == 4 ) EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 3 ), "" ); ASSERT_FALSE( lines[1].spans.empty() ); ASSERT_TRUE( lines[1].spans[0].text != nullptr ); ASSERT_FALSE( lines[1].spans[0].text->getString().empty() ); EXPECT_TRUE( lines[1].spans[0].text->getString()[0] != '\n' ); ASSERT_FALSE( lines[2].spans.empty() ); ASSERT_TRUE( lines[2].spans[0].text != nullptr ); ASSERT_FALSE( lines[2].spans[0].text->getString().empty() ); EXPECT_TRUE( lines[2].spans[0].text->getString()[0] != '\n' ); ASSERT_EQ( codeSpan->getHitBoxes().size(), (size_t)3 ); Float expectedLineHeight = pre->getLineHeightPx(); if ( expectedLineHeight <= 0.f ) { for ( const auto& span : lines[0].spans ) { if ( span.type == RichText::RenderSpan::Type::Text && span.text && !span.text->getString().empty() ) { const auto& fontStyle = span.text->getFontStyleConfig(); ASSERT_TRUE( fontStyle.Font != nullptr ); expectedLineHeight = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); break; } } } ASSERT_GT( expectedLineHeight, 0.f ); Float compactLineLimit = expectedLineHeight * 1.25f; for ( size_t i = 1; i < 3; ++i ) { Float lineDelta = lines[i].y - lines[i - 1].y; EXPECT_LE( lineDelta, compactLineLimit ); EXPECT_LE( lines[i].height, compactLineLimit ); const Rectf& prev = codeSpan->getHitBoxes()[i - 1]; const Rectf& cur = codeSpan->getHitBoxes()[i]; EXPECT_NEAR( cur.Top - prev.Top, lineDelta, 1.f ); EXPECT_LE( cur.Top - prev.Top, compactLineLimit ); } Engine::destroySingleton(); } UTEST( UIHTML, PreCodeFixtureDiscardsFinalNewlineBeforeEndTag ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre.code.2.html", html ) ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); SceneManager::instance()->update(); UIRichText* pre = nullptr; Node* code = nullptr; sceneNode->getRoot()->forEachNode( [&pre, &code]( Node* node ) { if ( node->isWidget() && node->asType()->getElementTag() == "pre" ) pre = node->asType(); if ( node->isWidget() && node->asType()->getElementTag() == "code" ) code = node; } ); ASSERT_TRUE( pre != nullptr ); ASSERT_TRUE( code != nullptr ); EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) ); const auto& lines = pre->getRichTextPtr()->getLines(); ASSERT_EQ( lines.size(), (size_t)3 ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "int x = 42;" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), "const char* y = \"hello\";" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "int z = foo(x, y);" ); Engine::destroySingleton(); } UTEST( UIHTML, PreCodeUsesCodeEditorOnlyForMarkdownAncestorOrGlobalOptIn ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"xml(
void main() {
    printf("Hello World");
}
)xml" ); SceneManager::instance()->update(); auto* markdownCode = sceneNode->getRoot()->find( "md_code" ); ASSERT_TRUE( markdownCode != nullptr ); ASSERT_TRUE( markdownCode->isType( UI_TYPE_CODEEDITOR ) ); EXPECT_TRUE( markdownCode->asType()->getDocument().getText().find( "printf" ) != String::InvalidPos ); Engine::destroySingleton(); init_ui_test(); sceneNode = SceneManager::instance()->getUISceneNode(); UIRichText::setUseCodeEditorForPreCodeBlocks( true ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
int value = 1;
)html" ) ); SceneManager::instance()->update(); auto* optInCode = sceneNode->getRoot()->find( "code" ); ASSERT_TRUE( optInCode != nullptr ); EXPECT_TRUE( optInCode->isType( UI_TYPE_CODEEDITOR ) ); UIRichText::setUseCodeEditorForPreCodeBlocks( false ); Engine::destroySingleton(); } UTEST( UIHTML, PreCodeBlockFixtureKeepsCompactCodeLines ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre_code_block.html", html ) ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); SceneManager::instance()->update(); UIRichText* pre = nullptr; sceneNode->getRoot()->forEachNode( [&pre]( Node* node ) { if ( pre == nullptr && node->isWidget() && node->asType()->getElementTag() == "pre" ) pre = node->asType(); } ); ASSERT_TRUE( pre != nullptr ); const auto& lines = pre->getRichTextPtr()->getLines(); ASSERT_EQ( lines.size(), (size_t)19 ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "let stepper () =" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), " // Execute a single instruction" ); EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 18 ), " mCycles " ); Float expectedLineHeight = pre->getLineHeightPx(); ASSERT_GT( expectedLineHeight, 0.f ); Float compactLineLimit = expectedLineHeight * 1.25f; for ( size_t i = 1; i < lines.size(); ++i ) { EXPECT_LE( lines[i].y - lines[i - 1].y, compactLineLimit ); EXPECT_LE( lines[i].height, compactLineLimit ); } std::vector codeLineSpans; for ( size_t i = 1; i <= 19; ++i ) { auto* lineSpan = sceneNode->getRoot()->find( String::format( "cb1-%zu", i ) ); ASSERT_TRUE( lineSpan != nullptr ); ASSERT_EQ( lineSpan->getHitBoxes().size(), (size_t)1 ); EXPECT_LE( lineSpan->getHitBoxes()[0].getHeight(), compactLineLimit ); codeLineSpans.push_back( lineSpan ); } for ( size_t i = 1; i < codeLineSpans.size(); ++i ) { const Rectf& prev = codeLineSpans[i - 1]->getHitBoxes()[0]; const Rectf& cur = codeLineSpans[i]->getHitBoxes()[0]; Float prevTop = codeLineSpans[i - 1]->getPixelsPosition().y + prev.Top; Float curTop = codeLineSpans[i]->getPixelsPosition().y + cur.Top; EXPECT_NEAR( curTop - prevTop, lines[i].y - lines[i - 1].y, 1.f ); EXPECT_LE( curTop - prevTop, compactLineLimit ); } Engine::destroySingleton(); } UTEST( UIHTML, WhiteSpaceCollapsePreLinePreservesBreaksOnly ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
alpha beta gamma
)html" ) ); SceneManager::instance()->update(); auto* preline = sceneNode->getRoot()->find( "preline" )->asType(); ASSERT_TRUE( preline != nullptr ); EXPECT_EQ( preline->getRichTextPtr()->getLines().size(), (size_t)2 ); EXPECT_STRINGEQ( uiHtmlLineText( *preline->getRichTextPtr(), 0 ), "alpha beta" ); EXPECT_STRINGEQ( uiHtmlLineText( *preline->getRichTextPtr(), 1 ), " gamma" ); Engine::destroySingleton(); } UTEST( UIHTML, WhiteSpaceCollapseBreakSpacesAffectsIntrinsicWidth ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
)html" ) ); SceneManager::instance()->update(); auto* normal = sceneNode->getRoot()->find( "normal" )->asType(); auto* breakSpaces = sceneNode->getRoot()->find( "breakspaces" )->asType(); ASSERT_TRUE( normal != nullptr ); ASSERT_TRUE( breakSpaces != nullptr ); EXPECT_LT( normal->getMinIntrinsicWidth(), breakSpaces->getMinIntrinsicWidth() ); EXPECT_GT( breakSpaces->getMinIntrinsicWidth(), 0.f ); EXPECT_STRINGEQ( uiHtmlRenderedText( *breakSpaces->getRichTextPtr() ), " " ); Engine::destroySingleton(); } UTEST( UIHTML, WhiteSpaceCollapsePreservedTabsUseTabSize ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
a	b
a	b
a	b
)html" ) ); SceneManager::instance()->update(); auto* tab2 = sceneNode->getRoot()->find( "tab2" )->asType(); auto* tab8 = sceneNode->getRoot()->find( "tab8" )->asType(); auto* tabpx = sceneNode->getRoot()->find( "tabpx" )->asType(); ASSERT_TRUE( tab2 != nullptr ); ASSERT_TRUE( tab8 != nullptr ); ASSERT_TRUE( tabpx != nullptr ); ASSERT_EQ( tab2->getRichTextPtr()->getLines().size(), (size_t)1 ); ASSERT_EQ( tab8->getRichTextPtr()->getLines().size(), (size_t)1 ); ASSERT_EQ( tabpx->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_STRINGEQ( uiHtmlLineText( *tab2->getRichTextPtr(), 0 ), "a\tb" ); EXPECT_STRINGEQ( uiHtmlLineText( *tab8->getRichTextPtr(), 0 ), "a\tb" ); EXPECT_STRINGEQ( uiHtmlLineText( *tabpx->getRichTextPtr(), 0 ), "a\tb" ); EXPECT_EQ( tab2->getTabSize(), 2u ); EXPECT_EQ( tab8->getTabSize(), 8u ); EXPECT_GT( tabpx->getTabSize(), 2u ); const Float tab2Width = tab2->getRichTextPtr()->getLines()[0].width; const Float tab8Width = tab8->getRichTextPtr()->getLines()[0].width; const Float tabpxWidth = tabpx->getRichTextPtr()->getLines()[0].width; EXPECT_GT( tab8Width, tab2Width ); EXPECT_GT( tabpxWidth, tab2Width ); ASSERT_FALSE( tab2->getRichTextPtr()->getLines()[0].spans.empty() ); ASSERT_FALSE( tab8->getRichTextPtr()->getLines()[0].spans.empty() ); ASSERT_FALSE( tabpx->getRichTextPtr()->getLines()[0].spans.empty() ); ASSERT_TRUE( tab2->getRichTextPtr()->getLines()[0].spans[0].text != nullptr ); ASSERT_TRUE( tab8->getRichTextPtr()->getLines()[0].spans[0].text != nullptr ); ASSERT_TRUE( tabpx->getRichTextPtr()->getLines()[0].spans[0].text != nullptr ); EXPECT_EQ( tab2->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(), 2u ); EXPECT_EQ( tab8->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(), 8u ); EXPECT_EQ( tabpx->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(), tabpx->getTabSize() ); Engine::destroySingleton(); } UTEST( UIRichText, anchorMargins ) { auto win = Engine::instance()->createWindow( WindowSettings( 800, 600, "Anchor Margins Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/anchor_margins.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); for ( auto anchor : anchors ) { auto a = anchor->asType(); EXPECT_EQ( anchor->getPixelsSize().getHeight(), a->getFont()->getLineSpacing( a->getFontSize() ) ); } compareImages( utest_state, utest_result, win, "eepp-ui-anchor-margins", "html" ); Engine::destroySingleton(); } UTEST( UIRichText, spanPadding ) { auto win = Engine::instance()->createWindow( WindowSettings( 800, 600, "Span Padding Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/span_padding.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-ui-span-padding", "html" ); Engine::destroySingleton(); } UTEST( UIRichText, anchorPadding ) { auto win = Engine::instance()->createWindow( WindowSettings( 800, 600, "Anchor Span Padding Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/anchor_padding.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-ui-anchor-padding", "html" ); auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); ASSERT_GE( anchors.size(), (size_t)1 ); auto downloadLink = anchors[0]->asType(); EXPECT_NEAR( downloadLink->getPixelsSize().getWidth(), 81.f, 3.f ); EXPECT_NEAR( downloadLink->getPixelsSize().getHeight(), 28.f, 3.f ); Engine::destroySingleton(); } UTEST( UIRichText, anchorPaddingLineHeight ) { auto win = Engine::instance()->createWindow( WindowSettings( 800, 600, "Anchor Padding LineHeight Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/anchor_padding_lineheight.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-ui-anchor-padding-lineheight", "html" ); auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); ASSERT_GE( anchors.size(), (size_t)1 ); auto downloadLink = anchors[0]->asType(); EXPECT_NEAR( downloadLink->getPixelsSize().getWidth(), 81.f, 3.f ); EXPECT_NEAR( downloadLink->getPixelsSize().getHeight(), 28.f, 3.f ); Engine::destroySingleton(); } UTEST( UIHTML, InlineBaselineAlignmentProperties ) { Engine::instance()->createWindow( WindowSettings( 800, 600, "Inline Baseline Alignment Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); sceneNode->getUIThemeManager()->setDefaultFont( font ); sceneNode->loadLayoutFromString( R"html( AX AX AX )html" ); sceneNode->updateDirtyLayouts(); auto* baseline = sceneNode->getRoot()->find( "baseline" )->asType(); auto* middle = sceneNode->getRoot()->find( "middle" )->asType(); auto* alignmentMiddle = sceneNode->getRoot()->find( "alignment_middle" )->asType(); auto* baselineBox = sceneNode->getRoot()->find( "base_box" )->asType(); auto* middleBox = sceneNode->getRoot()->find( "middle_box" )->asType(); auto* middleChild = sceneNode->getRoot()->find( "middle_child" )->asType(); auto* alignmentBox = sceneNode->getRoot()->find( "alignment_box" )->asType(); ASSERT_TRUE( baseline != nullptr ); ASSERT_TRUE( middle != nullptr ); ASSERT_TRUE( alignmentMiddle != nullptr ); ASSERT_TRUE( baselineBox != nullptr ); ASSERT_TRUE( middleBox != nullptr ); ASSERT_TRUE( middleChild != nullptr ); ASSERT_TRUE( alignmentBox != nullptr ); EXPECT_EQ( baselineBox->getBaselineAlign().type, CSSBaselineAlignment::Baseline ); EXPECT_EQ( middleBox->getBaselineAlign().type, CSSBaselineAlignment::Middle ); EXPECT_EQ( middleChild->getBaselineAlign().type, CSSBaselineAlignment::Baseline ); EXPECT_EQ( alignmentBox->getBaselineAlign().type, CSSBaselineAlignment::Middle ); Engine::destroySingleton(); } UTEST( UIHTML, InlineBlockVerticalAlignDoesNotInflateOwnTextLine ) { Engine::instance()->createWindow( WindowSettings( 800, 600, "Inline Block Vertical Align Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); sceneNode->getUIThemeManager()->setDefaultFont( font ); sceneNode->loadLayoutFromString( R"html( Aself.cpp Aself.cpp )html" ); sceneNode->updateDirtyLayouts(); auto* baseline = sceneNode->getRoot()->find( "baseline" )->asType(); auto* middle = sceneNode->getRoot()->find( "middle" )->asType(); auto* baselineBox = sceneNode->getRoot()->find( "base_box" )->asType(); auto* middleBox = sceneNode->getRoot()->find( "middle_box" )->asType(); ASSERT_TRUE( baseline != nullptr ); ASSERT_TRUE( middle != nullptr ); ASSERT_TRUE( baselineBox != nullptr ); ASSERT_TRUE( middleBox != nullptr ); ASSERT_EQ( baselineBox->getRichTextPtr()->getLines().size(), (size_t)1 ); ASSERT_EQ( middleBox->getRichTextPtr()->getLines().size(), (size_t)1 ); EXPECT_NEAR( middleBox->getPixelsSize().getHeight(), baselineBox->getPixelsSize().getHeight(), 0.5f ); EXPECT_NEAR( middleBox->getRichTextPtr()->getLines().front().height, baselineBox->getRichTextPtr()->getLines().front().height, 0.5f ); EXPECT_EQ( middleBox->getBaselineAlign().type, CSSBaselineAlignment::Middle ); EXPECT_GE( middle->getRichTextPtr()->getLines().front().height, baseline->getRichTextPtr()->getLines().front().height ); Engine::destroySingleton(); } UTEST( UIHTMLTable, complexLayout3 ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test 3", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/hn_frontpage.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout-3", "html" ); Engine::destroySingleton(); } UTEST( UIHTMLTable, nestedPerformance ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); // Create nested tables. UIHTMLTable* rootTable = UIHTMLTable::New(); rootTable->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); rootTable->setParent( sceneNode->getRoot() ); UIHTMLTable* currentTable = rootTable; for ( int i = 0; i < 10; ++i ) { UIHTMLTableRow* row = UIHTMLTableRow::New(); row->setParent( currentTable ); UIHTMLTableCell* cell = UIHTMLTableCell::New( "td" ); cell->setParent( row ); UIHTMLTable* childTable = UIHTMLTable::New(); childTable->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); childTable->setParent( cell ); currentTable = childTable; } UIHTMLTableRow* row = UIHTMLTableRow::New(); row->setParent( currentTable ); UIHTMLTableCell* cell = UIHTMLTableCell::New( "td" ); cell->setParent( row ); UITextSpan* span = UITextSpan::New(); span->setParent( cell ); span->setText( "Deeply nested text" ); Clock clock; sceneNode->updateDirtyLayouts(); Log::info( "Time for nested layout (10 levels): %.2f ms", clock.getElapsedTime().asMilliseconds() ); Engine::destroySingleton(); } UTEST( UIHTMLTable, specifiedWidth ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->loadLayoutFromString( R"(
C1 C2
)" ); sceneNode->updateDirtyLayouts(); auto c1 = sceneNode->getRoot()->find( "c1" ); auto c2 = sceneNode->getRoot()->find( "c2" ); ASSERT_TRUE( c1 != nullptr ); ASSERT_TRUE( c2 != nullptr ); // Cell 2 should be at least 200px. EXPECT_GE( c2->getPixelsSize().getWidth(), 200.f ); // Total width should be 500px (minus padding if any, but default is 0). EXPECT_NEAR( c1->getPixelsSize().getWidth() + c2->getPixelsSize().getWidth(), 500.f, 1.f ); Engine::destroySingleton(); } UTEST( UIHTMLTable, nestedSpecifiedWidth ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->loadLayoutFromString( R"(
Flexible text content that should take the rest of the space
)" ); sceneNode->updateDirtyLayouts(); auto c1 = sceneNode->getRoot()->find( "c1" ); auto c2 = sceneNode->getRoot()->find( "c2" ); ASSERT_TRUE( c1 != nullptr ); ASSERT_TRUE( c2 != nullptr ); // Cell 1 should be exactly 50px because it's rigid (only contains fixed-width image) // and Cell 2 is flexible. EXPECT_NEAR( c1->getPixelsSize().getWidth(), 50.f, 1.f ); EXPECT_NEAR( c2->getPixelsSize().getWidth(), 450.f, 1.f ); Engine::destroySingleton(); } UTEST( UIHTMLInput, sizeAttribute ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"html( )html" ); auto c1 = sceneNode->getRoot()->find( "i1" )->asType(); auto c2 = sceneNode->getRoot()->find( "i2" )->asType(); auto c3 = sceneNode->getRoot()->find( "i3" )->asType(); auto cp = sceneNode->getRoot()->find( "i_pwd" )->asType(); auto cm = sceneNode->getRoot()->find( "i_mode_pwd" )->asType(); auto cc = sceneNode->getRoot()->find( "i_chk" )->asType(); auto ccc = sceneNode->getRoot()->find( "i_chk_checked" )->asType(); auto crc = sceneNode->getRoot()->find( "i_radio_checked" )->asType(); ASSERT_TRUE( c1 != nullptr ); ASSERT_TRUE( c2 != nullptr ); ASSERT_TRUE( c3 != nullptr ); ASSERT_TRUE( cp != nullptr ); ASSERT_TRUE( cm != nullptr ); ASSERT_TRUE( cc != nullptr ); ASSERT_TRUE( ccc != nullptr ); ASSERT_TRUE( crc != nullptr ); auto i1 = c1->getChildWidget()->asType(); auto i2 = c2->getChildWidget()->asType(); auto i3 = c3->getChildWidget()->asType(); ASSERT_TRUE( i1 != nullptr ); ASSERT_TRUE( i2 != nullptr ); ASSERT_TRUE( i3 != nullptr ); EXPECT_EQ( i1->getHtmlSize(), 10u ); EXPECT_EQ( i2->getHtmlSize(), 20u ); EXPECT_EQ( i3->getHtmlSize(), 20u ); EXPECT_GT( i2->getPixelsSize().getWidth(), i1->getPixelsSize().getWidth() ); EXPECT_NEAR( i2->getPixelsSize().getWidth(), i3->getPixelsSize().getWidth(), 1.f ); EXPECT_TRUE( cp->getChildWidget()->isType( UI_TYPE_TEXTINPUT ) ); EXPECT_TRUE( cp->getChildWidget()->asType()->getMode() == UITextInput::TextInputMode::Password ); EXPECT_TRUE( cp->getChildWidget()->asType()->getMode() == UITextInput::TextInputMode::Password ); EXPECT_TRUE( cm->getChildWidget()->asType()->getMode() == UITextInput::TextInputMode::Password ); EXPECT_TRUE( cc->getChildWidget()->isType( UI_TYPE_CHECKBOX ) ); EXPECT_TRUE( ccc->getChildWidget()->isType( UI_TYPE_CHECKBOX ) ); EXPECT_TRUE( ccc->getChildWidget()->asType()->isChecked() ); EXPECT_TRUE( ccc->getFormValue() == "enabled" ); EXPECT_TRUE( crc->getChildWidget()->isType( UI_TYPE_RADIOBUTTON ) ); EXPECT_TRUE( crc->getChildWidget()->asType()->isActive() ); EXPECT_TRUE( crc->getFormValue() == "on" ); Engine::destroySingleton(); } UTEST( UIHTML, DataProperties ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
)html" ) ); auto* target = sceneNode->getRoot()->find( "target" )->asType(); ASSERT_TRUE( target != nullptr ); EXPECT_TRUE( target->hasDataProperty( "data-role" ) ); EXPECT_TRUE( target->hasDataProperty( "DATA-ROLE" ) ); EXPECT_TRUE( target->hasDataProperty( "data-empty" ) ); EXPECT_FALSE( target->hasDataProperty( "data-missing" ) ); EXPECT_TRUE( target->getDataPropertyString( "data-role" ) == "hero" ); EXPECT_TRUE( target->getDataPropertyString( "data-empty", "fallback" ) == "" ); EXPECT_TRUE( target->getDataPropertyString( "data-missing", "fallback" ) == "fallback" ); EXPECT_TRUE( target->getPropertyString( "data-role" ) == "hero" ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-role]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-role=\"hero\"]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-tags~=\"featured\"]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-lang|=\"en\"]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id^=\"user-\"]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id$=\"-42\"]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id*=\"ser\"]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-empty]" ).size(), (size_t)1 ); EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-missing]" ).size(), (size_t)0 ); EXPECT_TRUE( target->getDataPropertyString( "data-language" ) == "cpp" ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, closedByDefault ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Label

Content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* summary = sceneNode->getRoot()->find( "s" )->asType(); auto* content = sceneNode->getRoot()->find( "p" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( summary != nullptr ); ASSERT_TRUE( content != nullptr ); EXPECT_FALSE( details->isOpen() ); EXPECT_TRUE( summary->isVisible() ); EXPECT_FALSE( content->isVisible() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, summaryListStyleType ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Default Open marker Decimal marker
)html" ) ); sceneNode->updateDirtyLayouts(); const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); ASSERT_TRUE( propDef != nullptr ); auto* s1 = sceneNode->getRoot()->find( "s1" )->asType(); auto* s2 = sceneNode->getRoot()->find( "s2" )->asType(); auto* s3 = sceneNode->getRoot()->find( "s3" )->asType(); ASSERT_TRUE( s1 != nullptr ); ASSERT_TRUE( s2 != nullptr ); ASSERT_TRUE( s3 != nullptr ); EXPECT_TRUE( s1->getPropertyString( propDef ) == "disclosure-closed" ); EXPECT_TRUE( s2->getPropertyString( propDef ) == "disclosure-open" ); EXPECT_TRUE( s3->getPropertyString( propDef ) == "decimal" ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, summaryListStyleNoneClearsDefaultPadding ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Default marker No marker Explicit padding
)html" ) ); sceneNode->updateDirtyLayouts(); auto* defaultSummary = sceneNode->getRoot()->find( "default" )->asType(); auto* noneSummary = sceneNode->getRoot()->find( "none" )->asType(); auto* explicitSummary = sceneNode->getRoot()->find( "explicit" )->asType(); ASSERT_TRUE( defaultSummary != nullptr ); ASSERT_TRUE( noneSummary != nullptr ); ASSERT_TRUE( explicitSummary != nullptr ); EXPECT_GT( defaultSummary->getPixelsPadding().Left, 0.f ); EXPECT_NEAR( noneSummary->getPixelsPadding().Left, 0.f, 0.5f ); EXPECT_NEAR( explicitSummary->getPixelsPadding().Left, 7.f, 0.5f ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, inlineBlockSummaryListStyleNoneSize ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); std::string html; FileSystem::fileGet( "assets/html/lobsters_item.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->querySelector( ".caches" )->asType(); auto* summary = sceneNode->getRoot()->querySelector( ".caches summary" )->asType(); auto* author = sceneNode->getRoot()->querySelector( ".u-author" )->asType(); auto* time = sceneNode->getRoot()->querySelector( "time" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( summary != nullptr ); ASSERT_TRUE( author != nullptr ); ASSERT_TRUE( time != nullptr ); EXPECT_EQ( details->getDisplay(), CSSDisplay::InlineBlock ); EXPECT_EQ( summary->getListStyleType(), CSSListStyleType::None ); EXPECT_NEAR( summary->getPixelsPadding().Left, 0.f, 0.5f ); EXPECT_LE( details->getPixelsSize().getHeight(), eemax( author->getPixelsSize().getHeight(), time->getPixelsSize().getHeight() ) + 1.f ); EXPECT_NEAR( details->getPixelsPosition().y, author->getPixelsPosition().y, 2.f ); EXPECT_NEAR( details->getPixelsPosition().y, time->getPixelsPosition().y, 2.f ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, openAttribute ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Label

Content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* content = sceneNode->getRoot()->find( "p" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( content != nullptr ); EXPECT_TRUE( details->isOpen() ); EXPECT_TRUE( content->isVisible() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, openAttributeExplicitFalse ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Label

Content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* content = sceneNode->getRoot()->find( "p" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( content != nullptr ); EXPECT_FALSE( details->isOpen() ); EXPECT_FALSE( content->isVisible() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, toggleViaMouse ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Label

Content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* summary = sceneNode->getRoot()->find( "s" )->asType(); auto* content = sceneNode->getRoot()->find( "p" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( summary != nullptr ); ASSERT_TRUE( content != nullptr ); summary->onMouseClick( summary->getPixelsPosition().asInt(), EE_BUTTON_LMASK ); EXPECT_TRUE( details->isOpen() ); EXPECT_TRUE( content->isVisible() ); summary->onMouseClick( summary->getPixelsPosition().asInt(), EE_BUTTON_LMASK ); EXPECT_FALSE( details->isOpen() ); EXPECT_FALSE( content->isVisible() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, toggleViaKeyboard ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Label

Content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* summary = sceneNode->getRoot()->find( "s" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( summary != nullptr ); KeyEvent enter( summary, Event::KeyDown, KEY_RETURN, SCANCODE_RETURN, 0, 0 ); KeyEvent space( summary, Event::KeyDown, KEY_SPACE, SCANCODE_SPACE, 0, 0 ); EXPECT_EQ( summary->onKeyDown( enter ), 1u ); EXPECT_TRUE( details->isOpen() ); EXPECT_EQ( summary->onKeyDown( space ), 1u ); EXPECT_FALSE( details->isOpen() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, toggleEvent ) { init_ui_test(); auto* details = UIHTMLDetails::New(); details->setParent( SceneManager::instance()->getUISceneNode()->getRoot() ); int toggleCount = 0; details->on( Event::OnToggle, [&toggleCount]( const Event* ) { toggleCount++; } ); details->setOpen( true ); details->setOpen( true ); details->setOpen( false ); EXPECT_EQ( toggleCount, 2 ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, autoSummary ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(

Content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* content = sceneNode->getRoot()->find( "p" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( content != nullptr ); auto* summary = details->findSummaryChild(); ASSERT_TRUE( summary != nullptr ); EXPECT_TRUE( summary->isVisible() ); EXPECT_FALSE( content->isVisible() ); EXPECT_TRUE( summary->toggleParentDetails() ); EXPECT_TRUE( details->isOpen() ); EXPECT_TRUE( content->isVisible() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, multipleSummaries ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
One Two

Content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* s1 = sceneNode->getRoot()->find( "s1" )->asType(); auto* s2 = sceneNode->getRoot()->find( "s2" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( s1 != nullptr ); ASSERT_TRUE( s2 != nullptr ); EXPECT_TRUE( s1->isVisible() ); EXPECT_FALSE( s2->isVisible() ); EXPECT_TRUE( s1->toggleParentDetails() ); EXPECT_TRUE( details->isOpen() ); EXPECT_TRUE( s2->isVisible() ); EXPECT_FALSE( s2->toggleParentDetails() ); EXPECT_TRUE( details->isOpen() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, nested ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Outer
Inner

Inner content

)html" ) ); sceneNode->updateDirtyLayouts(); auto* outer = sceneNode->getRoot()->find( "outer" )->asType(); auto* inner = sceneNode->getRoot()->find( "inner" )->asType(); auto* innerSummary = sceneNode->getRoot()->find( "inner_s" )->asType(); ASSERT_TRUE( outer != nullptr ); ASSERT_TRUE( inner != nullptr ); ASSERT_TRUE( innerSummary != nullptr ); EXPECT_TRUE( outer->isOpen() ); EXPECT_FALSE( inner->isOpen() ); EXPECT_TRUE( innerSummary->toggleParentDetails() ); EXPECT_TRUE( outer->isOpen() ); EXPECT_TRUE( inner->isOpen() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, hiddenChildPreserved ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
Label
)html" ) ); sceneNode->updateDirtyLayouts(); auto* details = sceneNode->getRoot()->find( "d" )->asType(); auto* content = sceneNode->getRoot()->find( "p" )->asType(); ASSERT_TRUE( details != nullptr ); ASSERT_TRUE( content != nullptr ); EXPECT_FALSE( content->isVisible() ); details->setOpen( false ); details->setOpen( true ); EXPECT_FALSE( content->isVisible() ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, dynamicChildAddedWhileClosed ) { init_ui_test(); auto* details = UIHTMLDetails::New(); details->setParent( SceneManager::instance()->getUISceneNode()->getRoot() ); auto* summary = UIHTMLSummary::New(); summary->setParent( details ); details->setOpen( false ); auto* content = UIRichText::NewParagraph(); content->setParent( details ); details->updateLayout(); EXPECT_TRUE( summary->isVisible() ); EXPECT_FALSE( content->isVisible() ); Engine::destroySingleton(); } UTEST( UIHTMLTextArea, rowsColsAttribute ) { init_ui_test(); auto* scene = SceneManager::instance()->getUISceneNode(); auto* c1_raw = scene->loadLayoutFromString( R"html( )html" ); ASSERT_TRUE( c1_raw != nullptr ); auto* t1 = c1_raw->find( "t1" )->asType(); auto* t2 = c1_raw->find( "t2" )->asType(); ASSERT_TRUE( t1 != nullptr ); ASSERT_TRUE( t2 != nullptr ); EXPECT_EQ( t1->getRows(), 2u ); EXPECT_EQ( t1->getCols(), 20u ); EXPECT_EQ( t2->getRows(), 4u ); EXPECT_EQ( t2->getCols(), 40u ); EXPECT_GT( t2->getPixelsSize().getWidth(), t1->getPixelsSize().getWidth() ); EXPECT_GT( t2->getPixelsSize().getHeight(), t1->getPixelsSize().getHeight() ); Engine::destroySingleton(); } UTEST( UIHTML, FormControlsDefaultInlineBlock ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
)html" ) ); sceneNode->updateDirtyLayouts(); auto* i1 = sceneNode->getRoot()->find( "i1" )->asType(); auto* i2 = sceneNode->getRoot()->find( "i2" )->asType(); auto* t1 = sceneNode->getRoot()->find( "t1" )->asType(); auto* t2 = sceneNode->getRoot()->find( "t2" )->asType(); ASSERT_TRUE( i1 != nullptr ); ASSERT_TRUE( i2 != nullptr ); ASSERT_TRUE( t1 != nullptr ); ASSERT_TRUE( t2 != nullptr ); EXPECT_EQ( i1->getDisplay(), CSSDisplay::InlineBlock ); EXPECT_EQ( i2->getDisplay(), CSSDisplay::InlineBlock ); EXPECT_EQ( i1->getPixelsPosition().y, i2->getPixelsPosition().y ); EXPECT_LT( i1->getPixelsPosition().x, i2->getPixelsPosition().x ); EXPECT_EQ( t1->getPixelsPosition().y, t2->getPixelsPosition().y ); EXPECT_LT( t1->getPixelsPosition().x, t2->getPixelsPosition().x ); Engine::destroySingleton(); } UTEST( UIHTMLTable, tableLayoutFixed ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->loadLayoutFromString( R"(
C1 C2 C3
C4 (Should be ignored) C5 C6
)" ); sceneNode->updateDirtyLayouts(); auto c1 = sceneNode->getRoot()->find( "c1" ); auto c2 = sceneNode->getRoot()->find( "c2" ); auto c3 = sceneNode->getRoot()->find( "c3" ); ASSERT_TRUE( c1 != nullptr ); ASSERT_TRUE( c2 != nullptr ); ASSERT_TRUE( c3 != nullptr ); // Total width is 600px. C1=100, C2=200, C3 takes remaining 300px. EXPECT_NEAR( c1->getPixelsSize().getWidth(), 100.f, 1.f ); EXPECT_NEAR( c2->getPixelsSize().getWidth(), 200.f, 1.f ); EXPECT_NEAR( c3->getPixelsSize().getWidth(), 300.f, 1.f ); Engine::destroySingleton(); } UTEST( UIHTMLBody, backgroundColorPropagation ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->loadLayoutFromString( R"( )" ); sceneNode->updateDirtyLayouts(); auto html_el = sceneNode->getRoot()->find( "html_el" ); auto body_el = sceneNode->getRoot()->find( "body_el" ); ASSERT_TRUE( html_el != nullptr ); ASSERT_TRUE( body_el != nullptr ); // HTML element should have inherited the red background color, and body should be transparent EXPECT_TRUE( html_el->asType()->getBackgroundColor() == Color::Red ); EXPECT_TRUE( body_el->asType()->getBackgroundColor() == Color::Transparent ); Engine::destroySingleton(); } UTEST( UIHTMLBody, maxWidthResizingBug ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "HTML Resize Bug", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::CSS::StyleSheetParser parser; parser.loadFromFile( "assets/html/dwarmstrong/style.css" ); sceneNode->setStyleSheet( parser.getStyleSheet() ); std::string htmlContent; FileSystem::fileGet( "assets/html/dwarmstrong/dwarmstrong.html", htmlContent ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( htmlContent ) ); sceneNode->getRoot()->setSize( 1024, 768 ); sceneNode->updateDirtyLayouts(); auto body_el = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType(); ASSERT_TRUE( body_el != nullptr ); Float widthAt1024 = body_el->getPixelsSize().getWidth(); EXPECT_NEAR( widthAt1024, 960.f, 10.f ); // It should be around 960px (minus some margins if any) sceneNode->getRoot()->setSize( 2048, 768 ); sceneNode->updateDirtyLayouts(); Float widthAt2048 = body_el->getPixelsSize().getWidth(); EXPECT_NEAR( widthAt2048, 960.f, 10.f ); // Body should stay 960px even when parent is huge sceneNode->getRoot()->setSize( 1024, 768 ); sceneNode->updateDirtyLayouts(); Float widthAfterResize = body_el->getPixelsSize().getWidth(); EXPECT_NEAR( widthAt1024, widthAfterResize, 1.f ); Engine::destroySingleton(); } UTEST( UILayout, marginAuto ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "Margin Auto Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); auto* container = sceneNode->loadLayoutFromString( R"( )" ); auto child = sceneNode->getRoot()->find( "child" ); ASSERT_TRUE( child != nullptr ); UIWidget* childWidget = child->asType(); UIWidget* contWidget = container->asType(); contWidget->setSize( 500, 500 ); childWidget->setSize( 100, 100 ); sceneNode->updateDirtyLayouts(); Float expectedMarginX = ( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f; // Margin left/right should be auto computed to expectedMarginX EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f ); EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f ); EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Top, 0.f, 1.f ); EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Bottom, 0.f, 1.f ); // Resize parent and see if margins re-evaluate automatically contWidget->setSize( 800, 800 ); sceneNode->updateDirtyLayouts(); expectedMarginX = ( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f; EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f ); EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f ); // Now test resize of child childWidget->setSize( 200, 100 ); sceneNode->updateDirtyLayouts(); expectedMarginX = ( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f; EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f ); EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f ); childWidget->setLayoutMarginAuto( false, false, true, true ); sceneNode->updateDirtyLayouts(); Float expectedMarginY = ( contWidget->getPixelsSize().getHeight() - childWidget->getPixelsSize().getHeight() ) / 2.f; EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Top, expectedMarginY, 1.f ); EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Bottom, expectedMarginY, 1.f ); EXPECT_FALSE( childWidget->hasLayoutMarginLeftAuto() ); EXPECT_FALSE( childWidget->hasLayoutMarginRightAuto() ); EXPECT_TRUE( childWidget->hasLayoutMarginTopAuto() ); EXPECT_TRUE( childWidget->hasLayoutMarginBottomAuto() ); Engine::destroySingleton(); } UTEST( UILayout, listStyleTypeDecimal ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"html(
  1. First item
  2. Second item
  3. Third item
)html" ); sceneNode->updateDirtyLayouts(); const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); ASSERT_TRUE( propDef != nullptr ); auto* li1 = sceneNode->getRoot()->find( "li1" )->asType(); auto* li2 = sceneNode->getRoot()->find( "li2" )->asType(); auto* li3 = sceneNode->getRoot()->find( "li3" )->asType(); ASSERT_TRUE( li1 != nullptr ); ASSERT_TRUE( li2 != nullptr ); ASSERT_TRUE( li3 != nullptr ); EXPECT_TRUE( li1->getPropertyString( propDef ) == "decimal" ); EXPECT_TRUE( li2->getPropertyString( propDef ) == "decimal" ); EXPECT_TRUE( li3->getPropertyString( propDef ) == "decimal" ); Engine::destroySingleton(); } UTEST( UILayout, listStyleTypeDisc ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"html(
  • Bullet item
)html" ); sceneNode->updateDirtyLayouts(); const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); ASSERT_TRUE( propDef != nullptr ); auto* li1 = sceneNode->getRoot()->find( "li1" )->asType(); ASSERT_TRUE( li1 != nullptr ); EXPECT_TRUE( li1->getPropertyString( propDef ) == "disc" ); Engine::destroySingleton(); } UTEST( UILayout, listStyleTypeDisclosure ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"html(
  • Closed
  • Open
)html" ); sceneNode->updateDirtyLayouts(); const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); ASSERT_TRUE( propDef != nullptr ); auto* li1 = sceneNode->getRoot()->find( "li1" )->asType(); auto* li2 = sceneNode->getRoot()->find( "li2" )->asType(); ASSERT_TRUE( li1 != nullptr ); ASSERT_TRUE( li2 != nullptr ); EXPECT_TRUE( li1->getPropertyString( propDef ) == "disclosure-closed" ); EXPECT_TRUE( li2->getPropertyString( propDef ) == "disclosure-open" ); Engine::destroySingleton(); } UTEST( UILayout, listStyleShorthand ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"html(
  1. First
  2. Second
  3. Third
  • Bullet
  • Square
  • Circle
)html" ); sceneNode->updateDirtyLayouts(); const auto* typeDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); const auto* posDef = StyleSheetSpecification::instance()->getProperty( "list-style-position" ); for ( const char* id : { "li1", "li2", "li3", "li4", "li5", "li6" } ) { auto* li = sceneNode->getRoot()->find( id )->asType(); ASSERT_TRUE( li != nullptr ); EXPECT_TRUE( li->isType( UI_TYPE_HTML_LIST_ITEM ) ); } EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType()->getPropertyString( typeDef ) == "decimal" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType()->getPropertyString( posDef ) == "outside" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType()->getPropertyString( typeDef ) == "lower-alpha" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType()->getPropertyString( posDef ) == "inside" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li3" )->asType()->getPropertyString( typeDef ) == "none" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li4" )->asType()->getPropertyString( typeDef ) == "disc" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType()->getPropertyString( typeDef ) == "square" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType()->getPropertyString( posDef ) == "outside" ); EXPECT_TRUE( sceneNode->getRoot()->find( "li6" )->asType()->getPropertyString( typeDef ) == "circle" ); Engine::destroySingleton(); } UTEST( UILayout, listStyleInheritanceFromUl ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->loadLayoutFromString( R"html(
  1. Coffee
  • Coffee
  • Coffee
  • Coffee
  1. Coffee
)html" ); sceneNode->updateDirtyLayouts(); const auto* typeDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); EXPECT_TRUE( sceneNode->getRoot()->find( "h1" )->asType()->getPropertyString( typeDef ) == "upper-roman" ); EXPECT_TRUE( sceneNode->getRoot()->find( "a1" )->asType()->getPropertyString( typeDef ) == "circle" ); EXPECT_TRUE( sceneNode->getRoot()->find( "b1" )->asType()->getPropertyString( typeDef ) == "disc" ); EXPECT_TRUE( sceneNode->getRoot()->find( "c1" )->asType()->getPropertyString( typeDef ) == "square" ); EXPECT_TRUE( sceneNode->getRoot()->find( "d1" )->asType()->getPropertyString( typeDef ) == "decimal" ); Engine::destroySingleton(); } UTEST( UIHTMLDetails, lobstersInlineBlockCachesWidth ) { Engine::instance()->createWindow( WindowSettings( 424, 184, "HTML Details Lobsters Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html( )html" ) ); sceneNode->updateDirtyLayouts(); sceneNode->combineStyleSheet( R"css( body, textarea, input, button { font-family: "helvetica neue", arial, sans-serif; line-height: 1.45em; } ol.stories { padding: 0; list-style: none; margin: 0; } div.voters { float: left; text-align: center; width: 40px; } li.story { clear: both; } ol.stories li.story div.story_liner { padding-top: 0.25em; padding-bottom: 0.25em; word-break: break-word; } li div.details { padding-top: 0.1em; margin-left: 32px; } li .link { font-weight: bold; vertical-align: middle; } li .link a { text-decoration: none; } li.story a.tag { vertical-align: middle; } li .tags { margin-right: 0.25em; } li .domain { font-style: italic; text-decoration: none; vertical-align: middle; } img.avatar { border-radius: 8px; height: 16px; margin-bottom: 2px; margin-right: 2px; vertical-align: middle; width: 16px; } li.story .byline { margin-top: 1px; } .caches { display: inline-block; position: relative; } .caches summary { list-style: none; } .caches ul { position: absolute; white-space: nowrap; list-style: none; padding: 0; z-index: 1; } .caches a { text-decoration: none; display: block; padding: 3px 7px; } @media only screen and (max-width: 480px) { div#inside { margin: 0.5rem; } ol.stories { margin: 0 0 0 -0.5rem; padding-left: 0; } div.voters { margin-left: 0.25em; margin-top: 0px; width: 30px; } ol.stories.list { margin-top: 0; } ol.stories.list li.story { display: table; } ol.stories.list li.story div.story_liner { display: table-cell; padding-top: 0.5em; padding-bottom: 0.75em; width: 100%; } li div.details { margin: 0 0 0 36px; } } )css" ); sceneNode->updateDirtyLayouts(); auto* caches = sceneNode->getRoot()->querySelector( ".caches" )->asType(); auto* comments = sceneNode->getRoot()->querySelector( ".comments_label" ); auto* byline = sceneNode->getRoot()->querySelector( ".byline" ); EXPECT_LT( caches->getPixelsSize().getWidth(), byline->getPixelsSize().getWidth() * 0.5f ); EXPECT_NEAR( caches->getPixelsPosition().y, comments->getPixelsPosition().y, 1.f ); Engine::destroySingleton(); } UTEST( UIBorder, renderingVariations ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "Border Rendering Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/border_tests.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-ui-border-rendering", "html", 12 ); Engine::destroySingleton(); } UTEST( UIBorder, renderingVariations2 ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "Border Rendering Test 2", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/border_tests_2.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-ui-border-rendering-2", "html", 12 ); Engine::destroySingleton(); } static UISceneNode* init_test_inline_block() { FontTrueType* font = nullptr; FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); FontFamily::loadFromRegular( font ); FontTrueType* monoFont = FontTrueType::New( "monospace" ); monoFont->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); UISceneNode* sceneNode = UISceneNode::New(); SceneManager::instance()->add( sceneNode ); SceneManager::instance()->setCurrentUISceneNode( sceneNode ); UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); themeManager->applyDefaultTheme( sceneNode->getRoot() ); return sceneNode; } UTEST( UIHTML, InlineBlock ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Inline Block Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); const std::string html = R"html( )html"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); auto ul = sceneNode->getRoot()->findByTag( "ul" ); ASSERT_TRUE( ul != nullptr ); // Force layout update sceneNode->update( Seconds( 1 ) ); auto lis = ul->findAllByTag( "li" ); EXPECT_EQ( lis.size(), (size_t)6 ); for ( auto li : lis ) { EXPECT_EQ( li->asType()->getDisplay(), CSSDisplay::InlineBlock ); EXPECT_EQ( li->getLayoutWidthPolicy(), SizePolicy::WrapContent ); EXPECT_GT( li->getPixelsSize().getWidth(), 0 ); EXPECT_LT( li->getPixelsSize().getWidth(), ul->getPixelsSize().getWidth() ); EXPECT_GT( li->getPixelsSize().getHeight(), 0 ); } // Check if they are on the same line (inline-block) if ( lis.size() >= 2 ) { EXPECT_EQ( lis[0]->getPixelsPosition().y, lis[1]->getPixelsPosition().y ); EXPECT_LT( lis[0]->getPixelsPosition().x, lis[1]->getPixelsPosition().x ); } Engine::destroySingleton(); } UTEST( UIHTML, BlockList ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Block List Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); const std::string html = R"html(
  • Item 1
  • Item 2
)html"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); auto ul = sceneNode->getRoot()->findByTag( "ul" ); ASSERT_TRUE( ul != nullptr ); EXPECT_GT( ul->getPixelsSize().getWidth(), 0 ); auto lis = ul->findAllByTag( "li" ); EXPECT_EQ( lis.size(), (size_t)2 ); for ( auto li : lis ) { EXPECT_EQ( li->asType()->getDisplay(), CSSDisplay::ListItem ); EXPECT_GT( li->getChildCount(), (size_t)0 ); EXPECT_TRUE( li->asType()->getRichTextPtr()->getFontStyleConfig().Font != nullptr ); EXPECT_GT( li->asType()->getRichTextPtr()->getFontStyleConfig().CharacterSize, 0 ); EXPECT_GT( li->asType()->getRichTextPtr()->getSize().getWidth(), 0 ); EXPECT_GT( li->asType()->getRichTextPtr()->getSize().getHeight(), 0 ); EXPECT_GT( li->getPixelsSize().getWidth(), 0 ); EXPECT_GT( li->getPixelsSize().getHeight(), 0 ); } // They should be one above the other (block) EXPECT_LT( lis[0]->getPixelsPosition().y, lis[1]->getPixelsPosition().y ); EXPECT_EQ( lis[0]->getPixelsPosition().x, lis[1]->getPixelsPosition().x ); Engine::destroySingleton(); } UTEST( UIHTML, InlineList ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Inline List Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); const std::string html = R"html(
  • Item 1
  • Item 2
)html"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); auto ul = sceneNode->getRoot()->findByTag( "ul" ); ASSERT_TRUE( ul != nullptr ); EXPECT_GT( ul->getPixelsSize().getWidth(), 0 ); auto lis = ul->findAllByTag( "li" ); EXPECT_EQ( lis.size(), (size_t)2 ); for ( auto li : lis ) { EXPECT_EQ( li->asType()->getDisplay(), CSSDisplay::Inline ); EXPECT_EQ( li->getLayoutWidthPolicy(), SizePolicy::WrapContent ); EXPECT_GT( li->getPixelsSize().getWidth(), 0 ); EXPECT_LT( li->getPixelsSize().getWidth(), ul->getPixelsSize().getWidth() ); } // They should be on the same line (inline) EXPECT_EQ( lis[0]->getPixelsPosition().y, lis[1]->getPixelsPosition().y ); EXPECT_LT( lis[0]->getPixelsPosition().x, lis[1]->getPixelsPosition().x ); Engine::destroySingleton(); } UTEST( UIHTML, InlineBlockExplicitWidth ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Inline Block Explicit Width Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); const std::string html = R"html(
)html"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); auto d1 = sceneNode->getRoot()->find( "d1" )->asType(); auto d2 = sceneNode->getRoot()->find( "d2" )->asType(); ASSERT_TRUE( d1 != nullptr && d2 != nullptr ); // They should NOT be on the same line because 150 + 150 > 200 EXPECT_LT( d1->getPixelsPosition().y, d2->getPixelsPosition().y ); EXPECT_EQ( d1->getPixelsPosition().x, d2->getPixelsPosition().x ); Engine::destroySingleton(); } UTEST( UIHTML, InlineBlockMixedContent ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Mixed Content Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); const std::string html = R"html(
Some text
more text
)html"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); auto ib = sceneNode->getRoot()->find( "ib" )->asType(); ASSERT_TRUE( ib != nullptr ); // The inline-block should have a non-zero position and be somewhat centered vertically if it // follows text flow EXPECT_GT( ib->getPixelsPosition().x, 0 ); EXPECT_GT( ib->getPixelsSize().getWidth(), 0 ); Engine::destroySingleton(); } UTEST( UIHTML, InlineBlockWrapIssue ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Wrap Issue Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UI::UISceneNode* sceneNode = init_test_inline_block(); std::string html; FileSystem::fileGet( "assets/html/inline_block_wrap.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); auto h2 = sceneNode->getRoot()->find( "h2-wrap" ); ASSERT_TRUE( h2 != nullptr ); auto rt = h2->asType()->getRichTextPtr(); EXPECT_EQ( (size_t)1, rt->getLines().size() ); Engine::destroySingleton(); } UTEST( UIHTML, InlineBlockBrowserTest ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Browser Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UI::UISceneNode* sceneNode = init_test_inline_block(); std::string html; FileSystem::fileGet( "assets/html/is_inline_block.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); auto ib = sceneNode->getRoot()->find( "ib" )->asType(); auto t1 = sceneNode->getRoot()->find( "t1" )->asType(); auto t2 = sceneNode->getRoot()->find( "t2" )->asType(); ASSERT_TRUE( ib != nullptr && t1 != nullptr && t2 != nullptr ); // If it drops to the next line: EXPECT_GT( ib->getPixelsPosition().y, t1->getPixelsPosition().y ); // And t2 should be AFTER ib (either horizontally or vertically) EXPECT_GE( t2->getPixelsPosition().y, ib->getPixelsPosition().y + ib->getPixelsSize().getHeight() ); if ( t2->getPixelsPosition().y == ib->getPixelsPosition().y ) { EXPECT_GE( t2->getPixelsPosition().x, ib->getPixelsPosition().x + ib->getPixelsSize().getWidth() ); } EXPECT_EQ( ib->getPixelsPosition().x, t2->getPixelsPosition().x ); Engine::destroySingleton(); } UTEST( UIHTML, HeightExpansion ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Height Expansion Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/ensoft/" ); std::string html; FileSystem::fileGet( "assets/html/ensoft/ensoft.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); // Wait a bit and update again to make sure layouts are computed sceneNode->updateDirtyLayouts(); auto htmlNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_HTML ); auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY ); ASSERT_TRUE( htmlNode != nullptr ); ASSERT_TRUE( bodyNode != nullptr ); auto htmlWidget = htmlNode->asType(); auto bodyWidget = bodyNode->asType(); EXPECT_GT( htmlWidget->getSize().getHeight(), 0 ); EXPECT_GT( bodyWidget->getSize().getHeight(), 0 ); EXPECT_GE( htmlWidget->getSize().getHeight(), bodyWidget->getSize().getHeight() ); auto barNode = sceneNode->getRoot()->find( "bar" ); ASSERT_TRUE( barNode != nullptr ); auto barWidget = barNode->asType(); auto barHTML = barNode->asType(); EXPECT_EQ( barHTML->getCSSPosition(), CSSPosition::Fixed ); EXPECT_NEAR( barWidget->getPixelsSize().getWidth(), 250.f, 1.f ); auto rootWidget = sceneNode->getRoot(); EXPECT_GT( rootWidget->getPixelsSize().getHeight(), 0 ); EXPECT_NEAR( barWidget->getPixelsSize().getHeight(), rootWidget->getPixelsSize().getHeight(), 1.f ); Engine::destroySingleton(); } UTEST( UIHTML, HeightExpansion_FixedDoesNotExpand ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Height Expansion Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UI::UISceneNode* sceneNode = init_test_inline_block(); const std::string html = R"html(
)html"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY ); ASSERT_TRUE( bodyNode != nullptr ); auto bodyWidget = bodyNode->asType(); // The height should be 100px, not 550px because the fixed div should be ignored. EXPECT_NEAR( bodyWidget->getPixelsSize().getHeight(), 100.f, 1.f ); Engine::destroySingleton(); } UTEST( UIHTML, BodyHeightMiscalculationFixture ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Body Height Miscalculation Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/body_height_miscalculation.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY ); ASSERT_TRUE( bodyNode != nullptr ); auto bodyWidget = bodyNode->asType(); EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 3000.f ); EXPECT_LT( bodyWidget->getPixelsSize().getHeight(), 6000.f ); // Verify the nav layout: .wrap has justify-content: space-between, // so .brand should be at the left and .links at the right. auto nav = sceneNode->getRoot()->findByClass( "site-nav" ); ASSERT_TRUE( nav != nullptr ); auto navWidget = nav->asType(); auto wrap = navWidget->findByClass( "wrap" ); ASSERT_TRUE( wrap != nullptr ); auto wrapWidget = wrap->asType(); auto brand = wrapWidget->findByClass( "brand" ); ASSERT_TRUE( brand != nullptr ); auto brandWidget = brand->asType(); auto links = wrapWidget->findByClass( "links" ); ASSERT_TRUE( links != nullptr ); auto linksWidget = links->asType(); // With justify-content: space-between in a row-direction flex: // .brand should be at the left side of .wrap (near padding edge). EXPECT_LT( brandWidget->getPixelsPosition().x, wrapWidget->getPixelsSize().getWidth() * 0.25f ); // .links should be at the right side of .wrap, NOT next to .brand. Float wrapW = wrapWidget->getPixelsSize().getWidth(); Float linksW = linksWidget->getPixelsSize().getWidth(); EXPECT_GT( linksWidget->getPixelsPosition().x, wrapW * 0.5f ); // .links should NOT occupy all available width — it must be content-sized. EXPECT_GT( linksW, 0.f ); EXPECT_LT( linksW, wrapW * 0.5f ); Engine::destroySingleton(); } UTEST( UIHTML, ContactFormLayout ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Contact Form Layout Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/ensoft/" ); std::string html; FileSystem::fileGet( "assets/html/ensoft/contact.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto form = sceneNode->getRoot()->find( "form-contact" ); ASSERT_TRUE( form != nullptr ); auto formWidget = form->asType(); EXPECT_GT( formWidget->getPixelsSize().getHeight(), 0 ); auto ul = formWidget->findByTag( "ul" ); ASSERT_TRUE( ul != nullptr ); auto ulWidget = ul->asType(); Float ulHeight = ulWidget->getPixelsSize().getHeight(); EXPECT_GT( ulHeight, 0 ); auto lis = ulWidget->findAllByTag( "li" ); EXPECT_EQ( lis.size(), (size_t)7 ); Float totalLiHeight = 0; int visibleLiCount = 0; for ( auto li : lis ) { Float liH = li->getPixelsSize().getHeight(); if ( liH > 0 ) visibleLiCount++; totalLiHeight += liH; } EXPECT_EQ( visibleLiCount, 6 ); EXPECT_GT( totalLiHeight, 0 ); EXPECT_NEAR( ulHeight, totalLiHeight, 1.f ); auto contactBox = sceneNode->getRoot()->find( "contact-box" ); ASSERT_TRUE( contactBox != nullptr ); auto cbWidget = contactBox->asType(); EXPECT_GT( cbWidget->getPixelsSize().getHeight(), 10 ); auto content = sceneNode->getRoot()->find( "content" ); ASSERT_TRUE( content != nullptr ); auto contentWidget = content->asType(); EXPECT_GT( contentWidget->getPixelsSize().getHeight(), 60 ); auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY ); ASSERT_TRUE( bodyNode != nullptr ); auto bodyWidget = bodyNode->asType(); EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 0 ); Engine::destroySingleton(); } UTEST( UIBackground, imageAtlasPositioning ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "Background Atlas Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/background_atlas.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); // Verify the atlas image was actually loaded — scan all nodes for a loaded drawable bool foundLoadedImage = false; sceneNode->getRoot()->forEachNode( [&foundLoadedImage]( Node* node ) { if ( foundLoadedImage || !node->isWidget() ) return; auto* bg = node->asType()->getBackground(); if ( !bg ) return; auto* layer = bg->getLayer( 0 ); if ( layer && layer->getDrawable() ) { Sizef sz = layer->getDrawable()->getPixelsSize(); if ( sz.getWidth() == 1024.f && sz.getHeight() == 512.f ) foundLoadedImage = true; } } ); ASSERT_TRUE( foundLoadedImage ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-ui-background-atlas", "html", 4 ); Engine::destroySingleton(); } UTEST( UIBackground, inlineSpanColorRendersBehindBackgroundImage ) { auto win = Engine::instance()->createWindow( WindowSettings( 320, 160, "Inline Background Color Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/ensoft/" ); std::string html; FileSystem::fileGet( "assets/html/ensoft/background.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); auto anchors = sceneNode->getRoot()->querySelectorAll( "#rss a" ); ASSERT_EQ( anchors.size(), (size_t)1 ); auto* anchor = anchors.front()->asType(); ASSERT_TRUE( anchor != nullptr ); EXPECT_EQ( anchor->getFontBackgroundColor().getValue(), Color::fromString( "#d0d0d0" ).getValue() ); ASSERT_TRUE( anchor->getBackground() != nullptr ); ASSERT_TRUE( anchor->getBackground()->hasDrawableLayers() ); ASSERT_TRUE( anchor->getBackground()->getBackgroundDrawable().hasRadius() ); bool foundRoundedLayeredFragment = false; sceneNode->getRoot()->forEachNode( [&]( Node* node ) { if ( foundRoundedLayeredFragment || !node->isType( UI_TYPE_RICHTEXT ) ) return; for ( const auto& fragment : node->asType()->getRichText().getInlineFragments() ) { if ( fragment.type == RichText::InlineFragment::Type::Box && fragment.source.type == RichText::InlineSourceType::Widget && fragment.source.ptr == anchor ) { foundRoundedLayeredFragment = true; EXPECT_TRUE( fragment.backgroundColorDrawable != nullptr ); EXPECT_TRUE( fragment.backgroundDrawable != nullptr ); EXPECT_EQ( fragment.backgroundColor.getValue(), Color::fromString( "#d0d0d0" ).getValue() ); break; } } } ); EXPECT_TRUE( foundRoundedLayeredFragment ); const Rectf rect = anchor->getScreenRect(); ASSERT_GT( rect.getWidth(), 12.f ); ASSERT_GT( rect.getHeight(), 4.f ); Image image = win->getFrontBufferImage(); const Uint32 sampleX = static_cast( eefloor( rect.Right - 5.f ) ); const Uint32 sampleY = static_cast( eefloor( rect.Top + rect.getHeight() * 0.5f ) ); ASSERT_LT( sampleX, image.getWidth() ); ASSERT_LT( sampleY, image.getHeight() ); const Color pixel = image.getPixel( sampleX, sampleY ); EXPECT_NEAR( pixel.r, 0xd0, 4 ); EXPECT_NEAR( pixel.g, 0xd0, 4 ); EXPECT_NEAR( pixel.b, 0xd0, 4 ); Engine::destroySingleton(); } UTEST( UIBackground, imageAtlasPositioningPixelDensity2 ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "Background Atlas Test PD2", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); EE::Graphics::PixelDensity::setPixelDensity( 2.0f ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/background_atlas.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); compareImages( utest_state, utest_result, win, "eepp-ui-background-atlas-pd2", "html", 4 ); Engine::destroySingleton(); EE::Graphics::PixelDensity::setPixelDensity( 1.0f ); } UTEST( UIBackground, cssFileRelativeSpriteUrlAndNegativePosition ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
)html" ) ); sceneNode->combineStyleSheet( R"css( .sprite { width: 15px; height: 14px; background-image: url(sprite-reddit.13AvZYXRW_4.png); background-position: -42px -1678px; background-repeat: no-repeat; } )css", true, String::hash( "reddit-sprite-test" ), URI( "file://" + Sys::getProcessPath() + "assets/html/reddit_old_thread_files/reddit-sprite-test.css" ) ); sceneNode->updateDirtyLayouts(); auto* sprite = sceneNode->find( "sprite" ); ASSERT_TRUE( sprite != nullptr ); auto* background = sprite->getBackground(); ASSERT_TRUE( background != nullptr ); auto* layer = background->getLayer( 0 ); ASSERT_TRUE( layer != nullptr ); ASSERT_TRUE( layer->getDrawable() != nullptr ); EXPECT_EQ( layer->getDrawable()->getPixelsSize().getWidth(), 140 ); EXPECT_EQ( layer->getDrawable()->getPixelsSize().getHeight(), 1751 ); EXPECT_STDSTREQ( layer->getPositionX(), "-42px" ); EXPECT_STDSTREQ( layer->getPositionY(), "-1678px" ); EXPECT_NEAR( layer->getOffset().x, -42.f, 0.1f ); EXPECT_NEAR( layer->getOffset().y, -1678.f, 0.1f ); EXPECT_EQ( layer->getRepeatX(), UINodeDrawable::RepeatX::NoRepeat ); EXPECT_EQ( layer->getRepeatY(), UINodeDrawable::RepeatY::NoRepeat ); Engine::destroySingleton(); } UTEST( UIBackground, InlineBlockImageSpans ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "inline-block image spans", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/inline_block.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); auto spans = sceneNode->getRoot()->querySelectorAll( "a > span" ); EXPECT_GT( anchors.size(), (size_t)0 ); EXPECT_GT( spans.size(), (size_t)0 ); for ( auto anchor : anchors ) { EXPECT_GT( anchor->getPixelsSize().getWidth(), 0 ); EXPECT_GT( anchor->getPixelsSize().getHeight(), 0 ); } for ( auto span : spans ) { EXPECT_GT( span->getPixelsSize().getWidth(), 0 ); EXPECT_GT( span->getPixelsSize().getHeight(), 0 ); } compareImages( utest_state, utest_result, win, "eepp-ui-inline-block-image-spans", "html", 4 ); Engine::destroySingleton(); } UTEST( UIBackground, InlineBlockImageFixedSize ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "inline-block image fixed size", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/reddit_header_icons.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); EXPECT_GT( anchors.size(), (size_t)0 ); for ( auto anchor : anchors ) { EXPECT_EQ( anchor->getPixelsSize().getWidth(), 15 ); EXPECT_EQ( anchor->getPixelsSize().getHeight(), 12 ); } Engine::destroySingleton(); } UTEST( UIHTML, RedditHeaderPagenameBottomAlign ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "reddit header pagename bottom align", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/reddit_header.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->getInput()->update(); SceneManager::instance()->update(); auto* headerLeft = sceneNode->getRoot()->find( "header-bottom-left" )->asType(); auto* logo = sceneNode->getRoot()->find( "header-img" )->asType(); auto* page = sceneNode->getRoot()->querySelector( ".pagename" )->asType(); auto* jumpToContent = sceneNode->getRoot()->find( "jumpToContent" )->asType(); ASSERT_TRUE( headerLeft != nullptr ); ASSERT_TRUE( logo != nullptr ); ASSERT_TRUE( page != nullptr ); ASSERT_TRUE( jumpToContent != nullptr ); EXPECT_EQ( logo->getBaselineAlign().type, CSSBaselineAlignment::Bottom ); EXPECT_EQ( page->getBaselineAlign().type, CSSBaselineAlignment::Bottom ); EXPECT_NEAR( headerLeft->getPixelsSize().getHeight(), page->getPixelsPosition().y + page->getPixelsSize().getHeight(), 0.5f ); EXPECT_EQ( CSSPosition::Absolute, jumpToContent->getCSSPosition() ); EXPECT_NEAR( -865.f, jumpToContent->convertToWorldSpace( { 0, 0 } ).x, 1.f ); EXPECT_NEAR( 25.f, jumpToContent->convertToWorldSpace( { 0, 0 } ).y, 1.f ); Engine::destroySingleton(); } UTEST( UIHTML, AnchorsSizing ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "anchors sizing", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/lobsters_simple.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); EXPECT_GT( anchors.size(), (size_t)0 ); for ( auto anchor : anchors ) { auto a = anchor->asType(); if ( a->getDisplay() == CSSDisplay::None ) continue; EXPECT_GT( a->getPixelsSize().getHeight(), 0 ); if ( !a->getText().empty() && a->getFontStyleConfig().Font ) { String text = UIRichText::collapseInternalWhitespace( a->getText() ); text.trim(); if ( !text.empty() ) EXPECT_GE( a->getPixelsSize().getWidth(), Text::getTextWidth( text, a->getFontStyleConfig() ) ); } } Engine::destroySingleton(); } static UISceneNode* createWinAndLoadHTML( std::string winName, std::string htmlPath ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, winName, WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); if ( font == nullptr || !font->loaded() ) return nullptr; FontFamily::loadFromRegular( font ); UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( htmlPath, html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); return sceneNode; } UTEST( UIHTML, blockFlow ) { auto sceneNode = createWinAndLoadHTML( "HTML Block Flow", "assets/html/block_flow.html" ); ASSERT_TRUE( sceneNode != nullptr ); auto sections = sceneNode->getRoot()->findAllByClass( "language-section" ); ASSERT_EQ( sections.size(), (size_t)6 ); // Each section is display block so we expect a single section per line // if sections position are not equal it means that some sections are floating Float ref = sections[0]->getPixelsPosition().x; for ( auto section : sections ) EXPECT_EQ( section->getPixelsPosition().x, ref ); Engine::destroySingleton(); } UTEST( UIHTML, blockFlowFloat ) { auto sceneNode = createWinAndLoadHTML( "HTML Block Flow", "assets/html/block_flow_float_left.html" ); ASSERT_TRUE( sceneNode != nullptr ); auto sections = sceneNode->getRoot()->findAllByClass( "language-section" ); ASSERT_EQ( sections.size(), (size_t)6 ); // Each section is display block with float: left and width 48% so we expect two sections // per line, and each odd index should be to the right Float refLeft = sections[0]->getPixelsPosition().x; Float refRight = sections[1]->getPixelsPosition().x; for ( size_t idx = 0; idx < sections.size(); idx++ ) { Float expected = idx % 2 == 0 ? refLeft : refRight; EXPECT_EQ( sections[idx]->getPixelsPosition().x, expected ); } Engine::destroySingleton(); } UTEST( FontTrueType, glyphScaleZeroDimensionsNoCrash ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "Glyph Scale Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); ASSERT_TRUE( font != nullptr && font->loaded() ); FontFamily::loadFromRegular( font ); for ( unsigned int size : { 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u } ) { for ( Uint32 ch : { 'A', 'Z', 'a', 'z', '0', '9', '!', '@', '#', '$' } ) { const Glyph& glyph = font->getGlyph( ch, size, false, false, 0.f ); (void)glyph; } } UI::UISceneNode* sceneNode = UI::UISceneNode::New(); SceneManager::instance()->add( sceneNode ); UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); themeManager->setDefaultFont( font ); sceneNode->loadLayoutFromString( R"html( )html" ); sceneNode->updateDirtyLayouts(); auto tiny = sceneNode->getRoot()->find( "tiny" ); auto small = sceneNode->getRoot()->find( "small" ); auto normal = sceneNode->getRoot()->find( "normal" ); ASSERT_TRUE( tiny != nullptr ); ASSERT_TRUE( small != nullptr ); ASSERT_TRUE( normal != nullptr ); EXPECT_GT( normal->getPixelsSize().getWidth(), 0 ); EXPECT_GT( normal->getPixelsSize().getHeight(), 0 ); Engine::destroySingleton(); } UTEST( UIHTML, LiFloatLeft ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "li float left", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); UI::UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/float_li.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->setClearColor( Color::White ); win->getInput()->update(); SceneManager::instance()->update(); win->clear(); SceneManager::instance()->draw(); win->display(); auto livec = sceneNode->getRoot()->findAllByTag( "li" ); ASSERT_GT( livec.size(), (size_t)0 ); auto refY = livec[0]->getPixelsPosition().y; for ( size_t i = 1; i < livec.size(); i++ ) EXPECT_NEAR( refY, livec[i]->getPixelsPosition().y, 1.f ); Engine::destroySingleton(); } UTEST( UIHTML, FlexCenterWebViewLikeLayout ) { Engine::instance()->createWindow( WindowSettings( 1280, 720, "Flex Center WebView", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/flex_center.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto postWrapper = sceneNode->getRoot()->findByClass( "post-wrapper" ); auto post = sceneNode->getRoot()->findByClass( "post" ); ASSERT_TRUE( postWrapper != nullptr ); ASSERT_TRUE( post != nullptr ); auto postWidget = post->asType(); // In a real browser a div with width:100% inside a column flex container // with align-items:center still spans the full cross axis because 100% // resolves against the container. The item should therefore sit flush with // the container's left edge (plus any container padding). EXPECT_NEAR( postWidget->getPixelsPosition().x, 0.f, 2.f ); Engine::destroySingleton(); } UTEST( UIHTML, FlexCenterNoTextNodeDisplacement ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Center Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/flex_center.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto postWrapper = sceneNode->getRoot()->findByClass( "post-wrapper" ); ASSERT_TRUE( postWrapper != nullptr ); auto post = postWrapper->findByClass( "post" ); ASSERT_TRUE( post != nullptr ); // The post-wrapper is a column flex container. Whitespace text nodes // between the tags must not be treated as flex items, otherwise the // .post child would be displaced downward. auto postWrapperWidget = postWrapper->asType(); auto postWidget = post->asType(); Float postX = postWidget->getPixelsPosition().x; Float postY = postWidget->getPixelsPosition().y; Float wrapperTopPadding = postWrapperWidget->getPixelsPadding().Top; Float postMarginTop = postWidget->getLayoutPixelsMargin().Top; Float wrapperW = postWrapperWidget->getPixelsSize().getWidth(); Float postW = postWidget->getPixelsSize().getWidth(); // In a column flex container the first (and only) real flex item should // sit right after the container's top padding plus its own margin-top. EXPECT_NEAR( postY, wrapperTopPadding + postMarginTop, 2.f ); // The .post div has width: 100% so it should span the full cross axis. // With align-items: center there is no free space, so x should be at the // container's left padding (not displaced toward the right edge). EXPECT_NEAR( postX, 0.f, 2.f ); EXPECT_GT( postW, wrapperW * 0.5f ); Engine::destroySingleton(); } UTEST( UIHTML, BlockSizeInfDoesNotHang ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Block Size Inf Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/block_size_inf.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); // If we got here without hanging, the test passes. auto body = sceneNode->getRoot()->findByTag( "body" ); ASSERT_TRUE( body != nullptr ); auto bodyWidget = body->asType(); EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 0.f ); Engine::destroySingleton(); } UTEST( UIHTML, KittyHomeSmallDoesNotHang ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Kitty Home Small Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/kitty_home_small.html", html ); ASSERT_FALSE( html.empty() ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto body = sceneNode->getRoot()->findByTag( "body" ); ASSERT_TRUE( body != nullptr ); auto bodyWidget = body->asType(); EXPECT_GT( bodyWidget->getPixelsSize().getWidth(), 0.f ); EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 0.f ); Engine::destroySingleton(); } UTEST( UIHTML, FlexFormLayout ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Flex Form Layout Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/flex_form.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto form = sceneNode->getRoot()->findByClass( "newsletter-form" ); ASSERT_TRUE( form != nullptr ); auto formWidget = form->asType(); // The form has display: flex with no explicit height; it must derive // its height from the tallest flex item (the input or button). EXPECT_GT( formWidget->getPixelsSize().getHeight(), 0.f ); // Find the email input and subscribe button within the form. auto input = formWidget->findByTag( "input" ); auto button = formWidget->findByTag( "button" ); ASSERT_TRUE( input != nullptr ); ASSERT_TRUE( button != nullptr ); auto inputWidget = input->asType(); auto buttonWidget = button->asType(); // In a row flex container the button should sit to the right of the input. Float inputRight = inputWidget->getPixelsPosition().x + inputWidget->getPixelsSize().getWidth(); EXPECT_GT( buttonWidget->getPixelsPosition().x, inputRight - 1.f ); auto* buttonRichText = buttonWidget->isType( UI_TYPE_RICHTEXT ) ? buttonWidget->asType()->getRichTextPtr() : nullptr; ASSERT_TRUE( buttonRichText != nullptr ); buttonRichText->updateLayout(); EXPECT_EQ( buttonRichText->getLines().size(), 1u ); Engine::destroySingleton(); } UTEST( UIHTML, FlexMediaQueriesLayout ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Media Queries Layout Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/flex_mediaqueries.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto body = sceneNode->getRoot()->findByTag( "body" ); ASSERT_TRUE( body != nullptr ); auto bodyWidget = body->asType(); // Header auto header = bodyWidget->findByClass( "site-header" ); ASSERT_TRUE( header != nullptr ); auto headerWidget = header->asType(); // Header logo (contains SVG + "Causality" text) auto logo = headerWidget->findByClass( "header-logo" ); ASSERT_TRUE( logo != nullptr ); auto logoWidget = logo->asType(); // Header wordmark (the "Causality" span) auto wordmark = headerWidget->findByClass( "header-wordmark" ); ASSERT_TRUE( wordmark != nullptr ); auto wordmarkWidget = wordmark->asType(); // Site nav auto nav = headerWidget->findByClass( "site-nav" ); ASSERT_TRUE( nav != nullptr ); auto navWidget = nav->asType(); // Header should have visible height (padding + content + padding) EXPECT_GT( headerWidget->getPixelsSize().getHeight(), 80.f ); // Header should not exceed content width significantly EXPECT_LE( headerWidget->getPixelsSize().getWidth(), 1024.f ); // The wordmark "Causality" should have positive width and height (visible text) EXPECT_GT( wordmarkWidget->getPixelsSize().getWidth(), 10.f ); EXPECT_GT( wordmarkWidget->getPixelsSize().getHeight(), 5.f ); // The logo (flex item in header) should not extend outside the header EXPECT_LE( logoWidget->getPixelsPosition().x + logoWidget->getPixelsSize().getWidth(), headerWidget->getPixelsPosition().x + headerWidget->getPixelsSize().getWidth() + 1.f ); // Site nav should not overflow the header EXPECT_LE( navWidget->getPixelsPosition().x + navWidget->getPixelsSize().getWidth(), headerWidget->getPixelsPosition().x + headerWidget->getPixelsSize().getWidth() + 1.f ); // Essay nav auto essayNav = bodyWidget->findByClass( "essay-nav" ); ASSERT_TRUE( essayNav != nullptr ); auto essayNavWidget = essayNav->asType(); // The essay-nav link () auto essayNavLink = essayNavWidget->findByClass( "essay-nav-prev" ); ASSERT_TRUE( essayNavLink != nullptr ); auto essayNavLinkWidget = essayNavLink->asType(); // essay-nav should contain its link (link should not overflow in local coords) Float linkLocalX = essayNavLinkWidget->getPixelsPosition().x; Float linkLocalY = essayNavLinkWidget->getPixelsPosition().y; Float linkLocalW = essayNavLinkWidget->getPixelsSize().getWidth(); Float linkLocalH = essayNavLinkWidget->getPixelsSize().getHeight(); Float navW = essayNavWidget->getPixelsSize().getWidth(); Float navH = essayNavWidget->getPixelsSize().getHeight(); EXPECT_GE( linkLocalX, -1.f ); EXPECT_LE( linkLocalX + linkLocalW, navW + 1.f ); EXPECT_GE( linkLocalY, -1.f ); EXPECT_LE( linkLocalY + linkLocalH, navH + 1.f ); // essay-nav should have visible size EXPECT_GT( essayNavWidget->getPixelsSize().getHeight(), 10.f ); EXPECT_GT( essayNavWidget->getPixelsSize().getWidth(), 10.f ); // The essay-nav link contains two spans: label and title // They should stack vertically (flex-direction: column on the ) // so the link height should be at least the sum of both span heights auto essayLabel = essayNavLinkWidget->findByClass( "essay-nav-label" ); auto essayTitle = essayNavLinkWidget->findByClass( "essay-nav-title" ); if ( essayLabel && essayTitle ) { auto labelWidget = essayLabel->asType(); auto titleWidget = essayTitle->asType(); EXPECT_GT( essayNavLinkWidget->getPixelsSize().getHeight(), labelWidget->getPixelsSize().getHeight() + titleWidget->getPixelsSize().getHeight() - 1.f ); auto* labelRichText = essayLabel->isType( UI_TYPE_TEXTSPAN ) ? essayLabel->asType()->getRichTextPtr() : nullptr; auto* titleRichText = essayTitle->isType( UI_TYPE_TEXTSPAN ) ? essayTitle->asType()->getRichTextPtr() : nullptr; ASSERT_TRUE( labelRichText != nullptr ); ASSERT_TRUE( titleRichText != nullptr ); labelRichText->updateLayout(); titleRichText->updateLayout(); EXPECT_EQ( labelRichText->getLines().size(), 1u ); EXPECT_EQ( titleRichText->getLines().size(), 1u ); } Engine::destroySingleton(); } UTEST( UIHTML, FlexAnchorInFlexNavVisible ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Anchor in Flex Nav Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/flex_mediaquery.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto body = sceneNode->getRoot()->findByTag( "body" ); ASSERT_TRUE( body != nullptr ); auto bodyWidget = body->asType(); auto nav = bodyWidget->findByClass( "site-nav" ); ASSERT_TRUE( nav != nullptr ); auto navWidget = nav->asType(); // The nav is a flex container; its children are blockified flex items. // Each should have non-zero width and height (text must be visible). for ( Uint32 i = 0; i < navWidget->getChildCount(); ++i ) { auto c = navWidget->getChildAt( i ); auto cw = c->asType(); if ( !cw || cw->getElementTag() != "a" ) continue; EXPECT_GT( cw->getPixelsSize().getWidth(), 5.f ); EXPECT_GT( cw->getPixelsSize().getHeight(), 5.f ); } Engine::destroySingleton(); } UTEST( UIHTML, FlexTextSpanWrapContentUsesItemWidth ) { Engine::instance()->createWindow( WindowSettings( 1280, 720, "Flex Text Span Width Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/newsblur_home_prices_small.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto priceAmount = sceneNode->getRoot()->findByClass( "NB-pricing-tier-price-amount" ); ASSERT_TRUE( priceAmount != nullptr ); ASSERT_TRUE( priceAmount->isType( UI_TYPE_TEXTSPAN ) ); auto* amountSpan = priceAmount->asType(); auto* amountRichText = amountSpan->getRichTextPtr(); ASSERT_TRUE( amountRichText != nullptr ); amountRichText->updateLayout(); const auto& lines = amountRichText->getLines(); ASSERT_EQ( lines.size(), 1u ); ASSERT_EQ( lines.front().spans.size(), 1u ); ASSERT_EQ( lines.front().spans.front().type, RichText::RenderSpan::Type::Text ); ASSERT_TRUE( lines.front().spans.front().text != nullptr ); EXPECT_STRINGEQ( lines.front().spans.front().text->getString(), "$36" ); EXPECT_NEAR( lines.front().spans.front().position.x, 0.f, 1.f ); EXPECT_NEAR( amountSpan->getPixelsSize().getWidth(), lines.front().spans.front().size.x, 2.f ); Engine::destroySingleton(); } UTEST( UIHTML, FlexLiItemsWrapContentWidth ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex LI Width Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/flex_li_width.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); sceneNode->updateDirtyLayouts(); auto menuContainer = sceneNode->getRoot()->findByClass( "menu-container" ); ASSERT_TRUE( menuContainer != nullptr ); auto menuWidget = menuContainer->asType(); // The menu container is a flex row; its
  • children should wrap to // their content width, not span the full container width. Float containerWidth = menuWidget->getPixelsSize().getWidth(); Float containerPadding = menuWidget->getPixelsPadding().Left + menuWidget->getPixelsPadding().Right; Float contentWidth = containerWidth - containerPadding; Float totalLiWidth = 0.f; Uint32 liCount = 0; for ( Uint32 i = 0; i < menuWidget->getChildCount(); ++i ) { auto c = menuWidget->getChildAt( i ); if ( !c->isWidget() ) continue; auto cw = c->asType(); if ( cw->getElementTag() != "li" ) continue; // Each
  • should be narrower than the container content width EXPECT_LT( cw->getPixelsSize().getWidth(), contentWidth ); // Accumulate total width (including margin) Rectf margin = cw->getLayoutPixelsMargin(); totalLiWidth += cw->getPixelsSize().getWidth() + margin.Left + margin.Right; liCount++; } ASSERT_GT( liCount, (Uint32)0 ); // With 9 items and horizontal margins, the total should still fit without // each item spanning the full container. EXPECT_LT( totalLiWidth, contentWidth * (Float)liCount ); Engine::destroySingleton(); } UTEST( UIHTML, ImagePercentageWidthRespectsParentMaxWidth ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "img pct width respects parent max-width", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/image_width.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); auto articles = sceneNode->getRoot()->findAllByTag( "article" ); ASSERT_GT( articles.size(), (size_t)0 ); auto* articleWidget = articles[0]->asType(); ASSERT_TRUE( articleWidget != nullptr ); auto images = sceneNode->getRoot()->findAllByTag( "img" ); ASSERT_GT( images.size(), (size_t)0 ); auto* imgWidget = images[0]->asType(); ASSERT_TRUE( imgWidget != nullptr ); EXPECT_GT( articleWidget->getPixelsSize().getWidth(), 0.f ); EXPECT_GT( articleWidget->getPixelsSize().getHeight(), 0.f ); EXPECT_LE( articleWidget->getPixelsSize().getWidth(), 474.f ); EXPECT_GT( imgWidget->getPixelsSize().getWidth(), 0.f ); EXPECT_GT( imgWidget->getPixelsSize().getHeight(), 0.f ); EXPECT_LE( imgWidget->getPixelsSize().getWidth(), articleWidget->getPixelsSize().getWidth() ); auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); ASSERT_GT( anchors.size(), (size_t)0 ); auto* anchorWidget = anchors[0]->asType(); ASSERT_TRUE( anchorWidget != nullptr ); EXPECT_GT( anchorWidget->getPixelsSize().getWidth(), 0.f ); EXPECT_GT( anchorWidget->getPixelsSize().getHeight(), 0.f ); Engine::destroySingleton(); } UTEST( UIHTML, TextureReplaceInvalidatesRichTextAncestors ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "texture replace img relayout", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); UIWebView* webView = UIWebView::New(); webView->setParent( sceneNode->getRoot() ); webView->setPixelsSize( win->getWidth(), win->getHeight() ); webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); std::string html = R"html(

    Before image

    After image

    )html"; sceneNode->setURI( "file://delayed-image-resize.html" ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), webView->getDocumentContainer(), String::hash( "delayed-image-resize" ) ); win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); auto* article = sceneNode->getRoot()->find( "article" )->asType(); auto* body = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType(); auto* doc = webView->getDocumentContainer(); auto images = sceneNode->getRoot()->findAllByTag( "img" ); ASSERT_TRUE( article != nullptr ); ASSERT_TRUE( body != nullptr ); ASSERT_TRUE( doc != nullptr ); ASSERT_EQ( images.size(), (size_t)1 ); auto* img = images[0]->asType(); ASSERT_TRUE( img != nullptr ); Texture* texture = TextureFactory::instance()->createEmptyTexture( 1, 1, 4, Color::Transparent ); ASSERT_TRUE( texture != nullptr ); img->setDrawable( texture ); sceneNode->updateDirtyLayouts(); Float articleInitialHeight = article->getPixelsSize().getHeight(); Float bodyInitialHeight = body->getPixelsSize().getHeight(); Float docInitialHeight = doc->getPixelsSize().getHeight(); Image loadedImage( 320, 180, 4, Color::White ); texture->replace( &loadedImage ); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); EXPECT_GT( article->getPixelsSize().getHeight(), articleInitialHeight + 150.f ); EXPECT_GT( body->getPixelsSize().getHeight(), bodyInitialHeight + 150.f ); EXPECT_GT( doc->getPixelsSize().getHeight(), docInitialHeight + 150.f ); Engine::destroySingleton(); } UTEST( UIHTML, HtmlContainsTableBodyHeight ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "html contains table body height", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); UIWebView* webView = UIWebView::New(); webView->setParent( sceneNode->getRoot() ); webView->setPixelsSize( win->getWidth(), win->getHeight() ); webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); std::string html; ASSERT_TRUE( FileSystem::fileGet( "assets/html/hn_frontpage.html", html ) ); for ( std::string::size_type pos = 0; ( pos = html.find( "", pos ); ASSERT_TRUE( end != std::string::npos ); html.erase( pos, end + 2 - pos ); } sceneNode->setURI( "file://html-table-body-height.html" ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), webView->getDocumentContainer(), String::hash( "html-table-body-height" ) ); win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); std::string css; ASSERT_TRUE( FileSystem::fileGet( "assets/html/base.css", css ) ); css += "\n"; std::string newsCss; ASSERT_TRUE( FileSystem::fileGet( "assets/html/news.css", newsCss ) ); css += newsCss; sceneNode->runOnMainThread( [sceneNode, css = std::move( css )] { sceneNode->combineStyleSheet( css, true, String::hash( "html-table-body-height-late-css" ) ); } ); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); auto* htmlNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_HTML )->asType(); auto* body = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType(); auto* table = sceneNode->getRoot()->find( "hnmain" )->asType(); ASSERT_TRUE( htmlNode != nullptr ); ASSERT_TRUE( body != nullptr ); ASSERT_TRUE( table != nullptr ); EXPECT_GE( htmlNode->getPixelsSize().getHeight(), body->getPixelsSize().getHeight() ); EXPECT_GE( body->getPixelsSize().getHeight() + 4.f, table->getPixelsSize().getHeight() ); EXPECT_GT( table->getPixelsSize().getHeight(), 500.f ); Engine::destroySingleton(); } UTEST( UIHTML, DeferredCSSKeepsTableHeightStableAfterViewportResize ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "deferred css table height stable", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); UIWebView* webView = UIWebView::New(); webView->setParent( sceneNode->getRoot() ); webView->setPixelsSize( win->getWidth(), win->getHeight() ); webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); std::string html; ASSERT_TRUE( FileSystem::fileGet( "assets/html/hn_empty_thread.html", html ) ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), webView->getDocumentContainer(), String::hash( "hn-empty-thread-deferred-css" ) ); auto* titleCell = sceneNode->getRoot()->querySelector( ".title" ); ASSERT_TRUE( titleCell != nullptr ); ASSERT_TRUE( titleCell->isType( UI_TYPE_RICHTEXT ) ); const Color expectedTitleColor( "#828282" ); for ( int i = 0; i < 120 && titleCell->asType()->getFontColor() != expectedTitleColor; ++i ) { win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); Sys::sleep( Milliseconds( 5 ) ); } ASSERT_TRUE( titleCell->asType()->getFontColor() == expectedTitleColor ); win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); auto* bigboxTd = sceneNode->getRoot()->querySelector( "#bigbox > td" ); auto* bigboxTable = sceneNode->getRoot()->querySelector( "#bigbox > td > table" ); ASSERT_TRUE( bigboxTd != nullptr ); ASSERT_TRUE( bigboxTable != nullptr ); const Float initialTdHeight = bigboxTd->getPixelsSize().getHeight(); const Float initialTableHeight = bigboxTable->getPixelsSize().getHeight(); EXPECT_GT( initialTdHeight, 0.f ); EXPECT_GT( initialTableHeight, 0.f ); webView->setPixelsSize( win->getWidth(), win->getHeight() + 1 ); win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); EXPECT_NEAR( initialTdHeight, bigboxTd->getPixelsSize().getHeight(), 0.1f ); EXPECT_NEAR( initialTableHeight, bigboxTable->getPixelsSize().getHeight(), 0.1f ); Engine::destroySingleton(); } UTEST( UIHTML, ImageCSSWidthOverridesHTMLWidthAttribute ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "img css width overrides html width attr", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/image_width_2.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); auto images = sceneNode->getRoot()->findAllByTag( "img" ); ASSERT_GT( images.size(), (size_t)0 ); auto* imgWidget = images[0]->asType(); ASSERT_TRUE( imgWidget != nullptr ); auto mains = sceneNode->getRoot()->findAllByTag( "main" ); ASSERT_GT( mains.size(), (size_t)0 ); auto* mainWidget = mains[0]->asType(); ASSERT_TRUE( mainWidget != nullptr ); Float imgWidth = imgWidget->getPixelsSize().getWidth(); Float imgHeight = imgWidget->getPixelsSize().getHeight(); Float mainWidth = mainWidget->getPixelsSize().getWidth(); EXPECT_GT( imgWidth, 0.f ); EXPECT_GT( imgHeight, 0.f ); // CSS width: 100% should override HTML width="1500" // Image should be no wider than its container EXPECT_LE( imgWidth, mainWidth ); // Image should NOT be 1500 (the HTML attribute value) EXPECT_LT( imgWidth, 1500.f ); Engine::destroySingleton(); } UTEST( UIHTML, ImageMaxWidthConstrainsWebpWithHeightAuto ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "img max-width constrains webp with height auto", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); UISceneNode* sceneNode = init_test_inline_block(); sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); std::string html; FileSystem::fileGet( "assets/html/image_width_3.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); win->getInput()->update(); SceneManager::instance()->update(); sceneNode->updateDirtyLayouts(); auto articles = sceneNode->getRoot()->findAllByTag( "article" ); ASSERT_GT( articles.size(), (size_t)0 ); auto* articleWidget = articles[0]->asType(); ASSERT_TRUE( articleWidget != nullptr ); auto images = sceneNode->getRoot()->findAllByTag( "img" ); ASSERT_GT( images.size(), (size_t)0 ); auto* imgWidget = images[0]->asType(); ASSERT_TRUE( imgWidget != nullptr ); EXPECT_GT( articleWidget->getPixelsSize().getWidth(), 0.f ); EXPECT_GT( articleWidget->getPixelsSize().getHeight(), 0.f ); EXPECT_LE( articleWidget->getPixelsSize().getWidth(), 1140.f ); Float imgWidth = imgWidget->getPixelsSize().getWidth(); Float imgHeight = imgWidget->getPixelsSize().getHeight(); EXPECT_GT( imgWidth, 0.f ); EXPECT_GT( imgHeight, 0.f ); EXPECT_LE( imgWidth, articleWidget->getPixelsSize().getWidth() ); // max-width: 100% should prevent the image from using the HTML width="2560" EXPECT_LT( imgWidth, 2560.f ); // height should be proportional to width (aspect ratio: 1436/2560) Float expectedRatio = 1436.f / 2560.f; Float actualRatio = imgHeight / imgWidth; EXPECT_GT( actualRatio, expectedRatio * 0.9f ); EXPECT_LT( actualRatio, expectedRatio * 1.1f ); Engine::destroySingleton(); }