<!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>
