summaryrefslogtreecommitdiffstats
path: root/widget/tests/test_textScaleFactor_system_font.html
blob: bd2b55fbb6242321e7db0f84b7aaa5e846aafd79 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Test that system font sizing is independent from ui.textScaleFactor</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
  <style>
    p { width: max-content }
    #menu { font: menu }
  </style>
</head>
<body>
  <p id="menu">"menu" text.</p>
  <p id="default">Default text.</p>
</body>
<script>
"use strict";

const { AppConstants } = SpecialPowers.ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);

// Returns a Number for the font size in CSS pixels.
function elementFontSize(element) {
  return parseFloat(getComputedStyle(element).getPropertyValue("font-size"));
}

// "look-and-feel-changed" may be dispatched twice: once for the pref
// change and once after receiving the new values from
// ContentChild::RecvThemeChanged().
// pushPrefEnv() resolves after the former. This resolves after the latter.
function promiseNewFontSizeOnThemeChange(element) {
  return new Promise(resolve => {
    const lastSize = elementFontSize(element);

    function ThemeChanged() {
      const size = elementFontSize(element);
      if (size != lastSize) {
        resolve(size);
      }
    }
    // "look-and-feel-changed" is dispatched before the style system is flushed,
    // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/layout/base/nsPresContext.cpp#1684,1703-1705
    // so use an async observer to get a notification after style changes.
    SpecialPowers.addAsyncObserver(ThemeChanged, "look-and-feel-changed");
    SimpleTest.registerCleanupFunction(function() {
      SpecialPowers.removeAsyncObserver(ThemeChanged, "look-and-feel-changed");
    });
  });
}

function fuzzyCompareLength(actual, expected, message, tolerance, expectFn) {
  expectFn(Math.abs(actual-expected) <= tolerance,
           `${message} - got ${actual}, expected ${expected} +/- ${tolerance}`);
}

add_task(async () => {
  // MOZ_HEADLESS is set in content processes with GTK regardless of the
  // headless state of the parent process.  Check the parent state.
  const headless = await SpecialPowers.spawnChrome([], function get_headless() {
    return Services.env.get("MOZ_HEADLESS");
  });
  // LookAndFeel::TextScaleFactor::FloatID is implemented only for WINNT and
  // GTK.  ui.textScaleFactor happens to scale CSS pixels on other platforms
  // but system font integration has not been implemented.
  const expectSuccess = AppConstants.MOZ_WIDGET_TOOLKIT == "windows" ||
         (AppConstants.MOZ_WIDGET_TOOLKIT == "gtk" &&
          // Headless GTK doesn't get system font sizes from the system, but
          // uses sizes fixed in CSS pixels.
          !headless);

  async function setScaleAndPromiseFontSize(scale, element) {
    const prefPromise = SpecialPowers.pushPrefEnv({
      set: [["ui.textScaleFactor", scale]],
    });
    if (!expectSuccess) {
      // The size is not expected to change but get it afresh to check our
      // assumption.
      await prefPromise;
      return elementFontSize(element);
    }
    const [size] = await Promise.all([
      promiseNewFontSizeOnThemeChange(element),
      prefPromise,
    ]);
    return size;
  }

  const menu = document.getElementById("menu");
  const def = document.getElementById("default");
  // Choose a scaleFactor value different enough from possible default values
  // that app unit rounding does not prevent a change in devicePixelRatio.
  // A scaleFactor of 120 also has no rounding of app units per dev pixel.
  const referenceScale = 120;
  const menuSize1 = await setScaleAndPromiseFontSize(referenceScale, menu);
  const menuRect1 = menu.getBoundingClientRect();
  const defSize1 = elementFontSize(def);
  const defRect1 = def.getBoundingClientRect();

  const expectFn = expectSuccess ? ok : todo;
  const menuSize2 = await setScaleAndPromiseFontSize(2*referenceScale, menu);
  {
    const singlePrecisionULP = Math.pow(2, -23);
    // Precision seems to be lost in the conversion to decimal string for the
    // property value.
    const reltolerance = 30 * singlePrecisionULP;
    fuzzyCompareLength(menuSize2, menuSize1/2, "size of menu font",
                       reltolerance*menuSize1/2, expectFn);
  }
  {
    const menuRect2 = menu.getBoundingClientRect();
    // The menu font text renders exactly the same and app-unit rects are
    // equal, but the DOMRect conversion is rounded to 1/65536 CSS pixels.
    // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/dom/base/DOMRect.cpp#151-159
    // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1640441#c28
    const absTolerance = 1/65536
    fuzzyCompareLength(menuRect2.width, menuRect1.width/2,
                       "width of menu font <p> in px", absTolerance, expectFn);
    fuzzyCompareLength(menuRect2.height, menuRect1.height/2,
                       "height of menu font <p> in px", absTolerance, expectFn);
  }

  const defSize2 = elementFontSize(def);
  is(defSize2, defSize1, "size of default font");
  {
    const defRect2 = def.getBoundingClientRect();
    // Wider tolerance for hinting and snapping
    const relTolerance = 1/12;
    fuzzyCompareLength(defRect2.width, defRect1.width,
                       "width of default font <p> in px",
                       relTolerance*defRect1.width, ok);
    fuzzyCompareLength(defRect2.height, defRect1.height,
                       "height of default font <p> in px",
                       relTolerance*defRect1.height, ok);
  }
});
</script>
</html>