/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "gtest/gtest.h"
#include "gfxPlatform.h"
#include "gfxPlatformFontList.h"
#include "gfxTypes.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsTArray.h"
class FontFallbackTest : public ::testing::Test {
protected:
static void SetUpTestSuite() {
// Initialize gfxPlatform which also initializes the font list
gfxPlatform::GetPlatform();
}
static bool FontHasCharacter(gfxPlatformFontList* pfl,
const nsCString& fontName, uint32_t ch) {
nsAutoString text;
AppendUCS4ToUTF16(ch, text);
nsTArray<nsCString> fontList;
fontList.AppendElement(fontName);
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(text, fontList, fontsUsed);
if (fontsUsed.IsEmpty()) {
return false;
}
// Verify the returned font actually matches the requested one,
// not a global fallback font.
nsCString actualLower(fontsUsed[
0]);
ToLowerCase(actualLower);
nsCString expectedLower(fontName);
ToLowerCase(expectedLower);
return actualLower.Equals(expectedLower);
}
};
TEST_F(FontFallbackTest, ListFontsUsedForString_EmptyInput) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
nsTArray<nsCString> fontList;
fontList.AppendElement(
"DejaVu Sans"_ns);
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(nsAutoString(), fontList, fontsUsed);
EXPECT_EQ(fontsUsed.Length(),
0u) <<
"Empty text should use no fonts";
}
TEST_F(FontFallbackTest, ListFontsUsedForString_EmptyFontList) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
nsTArray<nsCString> emptyFontList;
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(u
"Hi"_ns, emptyFontList, fontsUsed);
EXPECT_EQ(fontsUsed.Length(),
0u) <<
"Empty font list should use no fonts";
}
TEST_F(FontFallbackTest, ListFontsUsedForString_BasicUsage) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
// Find a font that exists on this system
const char* commonFonts[] = {
"DejaVu Sans",
"Liberation Sans",
"Roboto",
"Noto Sans",
"Arial",
"Helvetica",
"Sans"};
nsCString workingFont;
for (
const char* fontNameStr : commonFonts) {
nsCString fontName(fontNameStr);
if (FontHasCharacter(pfl, fontName,
'A')) {
workingFont = fontName;
break;
}
}
if (workingFont.IsEmpty()) {
GTEST_SKIP() <<
"No common test fonts available on this system";
}
nsTArray<nsCString> fontList;
fontList.AppendElement(workingFont);
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(u
"Hello"_ns, fontList, fontsUsed);
EXPECT_EQ(fontsUsed.Length(),
1u)
<<
"Should use exactly one font for 'Hello'";
if (fontsUsed.Length() >
0) {
// The font name will be lowercased by GenerateFontListKey
nsCString expectedLower(workingFont);
ToLowerCase(expectedLower);
nsCString actualLower(fontsUsed[
0]);
ToLowerCase(actualLower);
EXPECT_EQ(actualLower, expectedLower);
}
}
TEST_F(FontFallbackTest, ListFontsUsedForString_NonExistentFont) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
nsTArray<nsCString> fontList;
fontList.AppendElement(
"This Font Does Not Exist 12345"_ns);
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(u
"A"_ns, fontList, fontsUsed);
// The non-existent font won't be found, but global fallback will find a
// system font to render 'A'.
EXPECT_EQ(fontsUsed.Length(),
1u)
<<
"Global fallback should find exactly one font for 'A'";
}
TEST_F(FontFallbackTest, ListFontsUsedForString_FallbackOrder) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
// Find two fonts that exist
const char* commonFonts[] = {
"DejaVu Sans",
"Liberation Sans",
"Roboto",
"Noto Sans",
"DejaVu Serif",
"Liberation Serif",
"Noto Serif",
"Nimbus Sans",
"Nimbus Roman",
"FreeSans",
"FreeSerif"};
nsTArray<nsCString> existingFonts;
for (
const char* fontNameStr : commonFonts) {
nsCString fontName(fontNameStr);
if (FontHasCharacter(pfl, fontName,
'A')) {
existingFonts.AppendElement(fontName);
if (existingFonts.Length() >=
2) {
break;
}
}
}
if (existingFonts.Length() <
2) {
GTEST_SKIP() <<
"Need at least 2 fonts for fallback order test";
}
// Test that first font in list wins
nsTArray<nsCString> fontList;
fontList.AppendElement(existingFonts[
0]);
fontList.AppendElement(existingFonts[
1]);
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(u
"A"_ns, fontList, fontsUsed);
EXPECT_EQ(fontsUsed.Length(),
1u);
if (fontsUsed.Length() >
0) {
nsCString expectedLower(existingFonts[
0]);
ToLowerCase(expectedLower);
nsCString actualLower(fontsUsed[
0]);
ToLowerCase(actualLower);
EXPECT_EQ(actualLower, expectedLower)
<<
"First font in list should be used when both have the character";
}
}
TEST_F(FontFallbackTest, ListFontsUsedForString_MultipleFontsWithEmoji) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
// U+1F600 = (grinning face)
const uint32_t grinningFace =
0x1F600;
// Find a text font that has 'A' but not emoji
const char* textFonts[] = {
"DejaVu Sans",
"Liberation Sans",
"Roboto",
"Noto Sans",
"DejaVu Serif",
"FreeSans",
"Nimbus Sans"};
// Find an emoji font that has emoji
const char* emojiFonts[] = {
"Noto Color Emoji",
"Noto Emoji",
"Twemoji",
"EmojiOne",
"Symbola",
"Segoe UI Emoji"};
nsCString textFont;
for (
const char* fontNameStr : textFonts) {
nsCString fontName(fontNameStr);
bool hasA = FontHasCharacter(pfl, fontName,
'A');
bool hasEmoji = FontHasCharacter(pfl, fontName, grinningFace);
// Want a font that has 'A' but NOT the emoji
if (hasA && !hasEmoji) {
textFont = fontName;
break;
}
}
nsCString emojiFont;
for (
const char* fontNameStr : emojiFonts) {
nsCString fontName(fontNameStr);
if (FontHasCharacter(pfl, fontName, grinningFace)) {
emojiFont = fontName;
break;
}
}
if (textFont.IsEmpty() || emojiFont.IsEmpty()) {
GTEST_SKIP() <<
"Need both a text font (without emoji) and an emoji font. "
<<
"textFont='"
<< (textFont.IsEmpty() ?
"(none)" : textFont.get()) <<
"' "
<<
"emojiFont='"
<< (emojiFont.IsEmpty() ?
"(none)" : emojiFont.get()) <<
"'";
}
// Test string: "Hi [grinning face]" - should need both fonts
nsTArray<nsCString> fontList;
fontList.AppendElement(textFont);
fontList.AppendElement(emojiFont);
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(u
"Hi \U0001F600"_ns, fontList, fontsUsed);
EXPECT_EQ(fontsUsed.Length(),
2u)
<<
"Should use exactly 2 fonts: one for text, one for emoji";
if (fontsUsed.Length() ==
2) {
// Verify the fonts are what we expect (order should be textFont first)
nsCString textFontLower(textFont);
ToLowerCase(textFontLower);
nsCString emojiFontLower(emojiFont);
ToLowerCase(emojiFontLower);
nsCString firstUsedLower(fontsUsed[
0]);
ToLowerCase(firstUsedLower);
nsCString secondUsedLower(fontsUsed[
1]);
ToLowerCase(secondUsedLower);
EXPECT_EQ(firstUsedLower, textFontLower)
<<
"First font used should be the text font";
EXPECT_EQ(secondUsedLower, emojiFontLower)
<<
"Second font used should be the emoji font";
}
}
TEST_F(FontFallbackTest, ListFontsUsedForString_VisibilityFilter) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
// Find a font that exists on this system
const char* commonFonts[] = {
"DejaVu Sans",
"Liberation Sans",
"Roboto",
"Noto Sans",
"Arial",
"Helvetica",
"Sans"};
nsCString workingFont;
for (
const char* fontNameStr : commonFonts) {
nsCString fontName(fontNameStr);
if (FontHasCharacter(pfl, fontName,
'A')) {
workingFont = fontName;
break;
}
}
if (workingFont.IsEmpty()) {
GTEST_SKIP() <<
"No common test fonts available on this system";
}
nsTArray<nsCString> fontList;
fontList.AppendElement(workingFont);
// Test with User visibility (default) - should work
nsTArray<nsCString> fontsUsedUser;
pfl->ListFontsUsedForString(u
"A"_ns, fontList, fontsUsedUser,
FontVisibility::User);
// The font should be found with User visibility (most permissive common case)
// Note: We can't easily predict font visibility, so we just verify the API
// works and returns consistent results
// Test with Base visibility - more restrictive
nsTArray<nsCString> fontsUsedBase;
pfl->ListFontsUsedForString(u
"A"_ns, fontList, fontsUsedBase,
FontVisibility::Base);
// With Base visibility, we either get the font (if it's a base font) or not
EXPECT_LE(fontsUsedBase.Length(),
1u)
<<
"With Base visibility, should have at most one font";
EXPECT_LE(fontsUsedUser.Length(),
1u)
<<
"With User visibility, should have at most one font";
}
TEST_F(FontFallbackTest, ListFontsUsedForString_DefaultVisibility) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
const char* commonFonts[] = {
"DejaVu Sans",
"Liberation Sans",
"Roboto",
"Noto Sans",
"Arial",
"Helvetica",
"Sans"};
nsCString workingFont;
for (
const char* fontNameStr : commonFonts) {
nsCString fontName(fontNameStr);
if (FontHasCharacter(pfl, fontName,
'A')) {
workingFont = fontName;
break;
}
}
if (workingFont.IsEmpty()) {
GTEST_SKIP() <<
"No common test fonts available on this system";
}
nsTArray<nsCString> fontList;
fontList.AppendElement(workingFont);
// Call without visibility parameter (should use default User)
nsTArray<nsCString> fontsUsedDefault;
pfl->ListFontsUsedForString(u
"Test"_ns, fontList, fontsUsedDefault);
// Call with explicit User visibility
nsTArray<nsCString> fontsUsedUser;
pfl->ListFontsUsedForString(u
"Test"_ns, fontList, fontsUsedUser,
FontVisibility::User);
// Results should be identical
EXPECT_EQ(fontsUsedDefault.Length(), fontsUsedUser.Length())
<<
"Default visibility should match explicit User visibility";
}
TEST_F(FontFallbackTest, ListFontsUsedForString_VisibilityMonotonicity) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
const char* commonFonts[] = {
"DejaVu Sans",
"Liberation Sans",
"Roboto",
"Noto Sans",
"Arial",
"Helvetica",
"Sans"};
nsCString workingFont;
for (
const char* fontNameStr : commonFonts) {
nsCString fontName(fontNameStr);
if (FontHasCharacter(pfl, fontName,
'A')) {
workingFont = fontName;
break;
}
}
if (workingFont.IsEmpty()) {
GTEST_SKIP() <<
"No common test fonts available on this system";
}
nsTArray<nsCString> fontList;
fontList.AppendElement(workingFont);
// Test all visibility levels from most restrictive to least
FontVisibility levels[] = {FontVisibility::Base, FontVisibility::LangPack,
FontVisibility::User, FontVisibility::Hidden};
size_t prevFontsUsed =
0;
for (FontVisibility vis : levels) {
nsTArray<nsCString> fontsUsed;
pfl->ListFontsUsedForString(u
"Hello"_ns, fontList, fontsUsed, vis);
// More permissive visibility should have at least as many fonts available
EXPECT_GE(fontsUsed.Length(), prevFontsUsed)
<<
"Visibility level " << static_cast<
int>(vis)
<<
" should not have fewer fonts than more restrictive levels";
prevFontsUsed = fontsUsed.Length();
}
}
TEST_F(FontFallbackTest, ListFontsUsedForString_VisibilityWithMultipleFonts) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
// Find multiple fonts that exist
const char* commonFonts[] = {
"DejaVu Sans",
"Liberation Sans",
"Roboto",
"Noto Sans",
"DejaVu Serif",
"Liberation Serif",
"Noto Serif",
"Nimbus Sans",
"FreeSans",
"FreeSerif"};
nsTArray<nsCString> existingFonts;
for (
const char* fontNameStr : commonFonts) {
nsCString fontName(fontNameStr);
if (FontHasCharacter(pfl, fontName,
'A')) {
existingFonts.AppendElement(fontName);
if (existingFonts.Length() >=
3) {
break;
}
}
}
if (existingFonts.Length() <
2) {
GTEST_SKIP() <<
"Need at least 2 fonts for this test";
}
// Test with Hidden visibility (most permissive)
nsTArray<nsCString> fontsUsedHidden;
pfl->ListFontsUsedForString(u
"ABC"_ns, existingFonts, fontsUsedHidden,
FontVisibility::Hidden);
// With multiple fonts all having 'A', 'B', 'C', only the first should be used
// (if that font covers all characters)
EXPECT_GE(fontsUsedHidden.Length(),
1u)
<<
"Should use at least one font for basic Latin chars";
}
TEST_F(FontFallbackTest, ListFontsUsedForString_VariantI_EmojiFontList) {
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
ASSERT_NE(pfl, nullptr);
// Emoji font list from populateSVGRect's CSS_FONT_FAMILY
nsTArray<nsCString> emojiFontList;
emojiFontList.AppendElement(
"Apple Color Emoji"_ns);
emojiFontList.AppendElement(
"Segoe UI Emoji"_ns);
emojiFontList.AppendElement("Segoe UI Symbol"_ns);
emojiFontList.AppendElement("Noto Color Emoji"_ns);
emojiFontList.AppendElement("EmojiOne Color"_ns);
emojiFontList.AppendElement("Android Emoji"_ns);
emojiFontList.AppendElement("sans-serif"_ns);
// A few emojis from the SVG list
nsAutoString emojiText;
AppendUCS4ToUTF16(
0x1F600, emojiText); // Grinning Face
AppendUCS4ToUTF16(
0x263A, emojiText); // White Smiling Face
AppendUCS4ToUTF16(
0x2708, emojiText); // Airplane
nsTArray<nsCString> fontsAllowlisted;
pfl->ListFontsUsedForString(emojiText, emojiFontList, fontsAllowlisted,
FontVisibility::LangPack);
nsTArray<nsCString> fontsNonAllowlisted;
pfl->ListFontsUsedForString(emojiText, emojiFontList, fontsNonAllowlisted,
FontVisibility::User);
// User visibility should find at least as many fonts as LangPack
EXPECT_GE(fontsNonAllowlisted.Length(), fontsAllowlisted.Length());
// On any platform with emoji support, at least one font should be found
EXPECT_GT(fontsNonAllowlisted.Length(),
0u)
<< "Should find at least one emoji font for basic emoji codepoints";
}