chromium/third_party/blink/web_tests/inspector-protocol/layout-fonts/resources/style-matching-test.js

(async function test(testRunner) {
  var {session, dp} = await testRunner.startHTML(`
    <!DOCTYPE html>
    <html>
    <meta charset="UTF-8">
    <head>
        <!-- available fonts --->
        <style type="text/css">
        </style>
        <link rel="stylesheet" type="text/css" href="${testRunner.url("resources/applied-styles.css")}">
    </head>
    <body>
        <div class="test">
            <div id="condensed_normal_100">abcdefg</div>
            <div id="condensed_normal_900">abcdefg</div>
            <div id="condensed_italic_100">abcdefg</div>
            <div id="condensed_italic_900">abcdefg</div>
            <div id="expanded_normal_100">abcdefg</div>
            <div id="expanded_normal_900">abcdefg</div>
            <div id="expanded_italic_100">abcdefg</div>
            <div id="expanded_italic_900">abcdefg</div>
        </div>
    </body>
    </html>
  `, `According to the CSS3 Fonts Module, Step 4 or the Font Style Matching Algorithm
( https://drafts.csswg.org/css-fonts-3/#font-style-matching ) must narrow down the
available font faces by finding the nearest match in the following order of
precedence: stretch, style, weight.`);

  await dp.DOM.enable();
  await dp.CSS.enable();

  await session.evaluate(`
    function updateFont(available) {
      var fulfill;
      var promise = new Promise(f => fulfill = f);

      function emptyFontFaceDeclarations()
      {
          var stylesheet = document.styleSheets[0];
          var rules = stylesheet.cssRules;
          var rulesLengthSnapshot = stylesheet.cssRules.length;
          if (!rulesLengthSnapshot)
              return;
          for (var i = 0; i < rulesLengthSnapshot; i++) {
              stylesheet.deleteRule(0);
          }
      }

      function makeFontFaceDeclaration(stretch, style, weight)
      {
          var fontFaceDeclaration = '@font-face { font-family: "CSSMatchingtest"; ' +
              "font-stretch: " + stretch + ";" +
              "font-style: " + style + ";" +
              "font-weight: " + weight + ";" +
              'src: url("' +
              "${testRunner.url("resources/fonts/CSSMatchingTest_")}" +
              stretch + "_" + style + "_" + weight + '.ttf");';
          return fontFaceDeclaration;
      }

      function notifyInspectorFontsReady() {
          var cssRules = document.styleSheets[0].cssRules;
          var fontsAvailable = [];
          for (var i = 0; i < cssRules.length; i++) {
              urlmatch = \/url\\(".*fonts\\/CSSMatchingTest_(.*).ttf"\\)/.exec(
                  cssRules[i].cssText);
              fontsAvailable.push(urlmatch[1]);
          }
          fulfill(JSON.stringify(fontsAvailable));
      }

      function updateAvailableFontFaceDeclarations(available)
      {
          emptyFontFaceDeclarations();
          for (stretch of available.stretches)
              for (style of available.styles)
                  for (weight of available.weights) {
                      document.styleSheets[0].addRule(
                          makeFontFaceDeclaration(stretch, style, weight));
                  }

          document.fonts.ready.then(() => {
              // fonts.ready event fires too early, used fonts for rendering have not
              // been updated yet. Force a layout to hopefully work around this.
              // Remove this when crbug.com/516680 is fixed.
              // https://drafts.csswg.org/css-font-loading/#font-face-set-ready
              document.body.offsetTop;
              notifyInspectorFontsReady();
          });
      }
      updateAvailableFontFaceDeclarations(available);
      return promise;
    }
  `);

  var documentNodeId;
  var documentNodeSelector;
  var allTestSelectors = [];
  var testSelectors = [];
  var testGroup = 0;

  var stretches = ['condensed', 'expanded'];
  var styles = ['normal', 'italic'];
  var weights = ['100', '900'];

  var weightsHumanReadable = ["Thin", "Black"];

  var fontSetVariations = [];
  var currentFontSetVariation = {};

  makeFontSetVariations();

  function makeFontSetVariations() {
    // For each of the three properties we have three choices:
    // Restrict to the first value, to the second, or
    // allow both. So we're iterating over those options
    // for each dimension to generate the set of allowed
    // options for each property. The fonts in the test
    // page will later be generated from these possible
    // choices / restrictions.
    function choices(property) {
      return [property, [property[0]], [property[1]]];
    }

    for (var stretchChoice of choices(stretches)) {
      for (var styleChoice of choices(styles)) {
        for (var weightChoice of choices(weights)) {
          var available = {};
          available.stretches = stretchChoice;
          available.styles = styleChoice;
          available.weights = weightChoice;
          fontSetVariations.push(available);
        }
      }
    }
  }

  function subsetFontSetVariations() {
    var NUM_GROUPS = 9;
    var numVariations = fontSetVariations.length;
    var groupLength = numVariations / NUM_GROUPS;
    var start = testGroup * groupLength;
    var end = start + groupLength;
    testRunner.log("Testing font set variations " + start + " to " +
      (end - 1) + " out of 0-" + (numVariations - 1));
    fontSetVariations = fontSetVariations.slice(start, end);
  }

  dp.DOM.getDocument({}).then(({ result }) => onDocumentNodeId(result.root.nodeId));

  function onDocumentNodeId(nodeId) {
    documentNodeId = nodeId;
    var params = new URLSearchParams(window.location.search);
    var testURL = params.get('test');
    testGroup = /font-style-matching-(\d).js/.exec(
      testURL)[1];
    subsetFontSetVariations();
    session.evaluate(
      'JSON.stringify(' +
      'Array.prototype.map.call(' +
      'document.querySelectorAll(".test *"),' +
      'function(element) { return element.id } ));',
    ).then(startTestingPage);
  }

  function startTestingPage(result) {
    allTestSelectors = JSON.parse(result);
    nextTest();
  }

  function nextTest() {
    if (testSelectors.length) {
      testNextPageElement();
    } else {
      currentFontSetVariation = fontSetVariations.shift();
      if (currentFontSetVariation) {
        testSelectors = allTestSelectors.slice();
        updateFontDeclarationsInPageForVariation(
          currentFontSetVariation);
      } else {
        testRunner.completeTest();
      }
    }
  }

  async function updateFontDeclarationsInPageForVariation(fontSetVariation) {
    var loadedFonts = await session.evaluateAsync(
      'updateFont(' +
      JSON.stringify(fontSetVariation) +
      ');');
    testRunner.log("Available fonts updated: " + loadedFonts + "\n");
    nextTest();
  }

  function testNextPageElement(result) {
    var nextSelector = testSelectors.shift()
    if (nextSelector) {
      documentNodeSelector = "#" + nextSelector;
      platformFontsForElementWithSelector(documentNodeSelector);
    }
  }

  async function platformFontsForElementWithSelector(selector) {
    var response = await dp.DOM.querySelector({ nodeId: documentNodeId, selector });
    var nodeId = response.result.nodeId;
    var response = await dp.CSS.getPlatformFontsForNode({ nodeId });
    logResults(response);
    nextTest();
  }

  function logResults(response) {
    testRunner.log(documentNodeSelector);
    logPassFailResult(
      documentNodeSelector,
      response.result.fonts[0].familyName);
  }

  function cssStyleMatchingExpectationForSelector(selector) {
    var selectorStretchStyleWeight = selector.substr(1).split("_");
    var selectorStretch = selectorStretchStyleWeight[0].toLowerCase();
    var selectorStyle = selectorStretchStyleWeight[1].toLowerCase();
    var selectorWeight = selectorStretchStyleWeight[2];

    var expectedProperties = {};
    if (currentFontSetVariation.stretches.indexOf(selectorStretch) > 0) {
      expectedProperties.stretch = selectorStretch;
    } else {
      // If the requested property value is not availabe in the
      // current font set, then it's restricted to only one value,
      // which is the nearest match, and at index 0.
      expectedProperties.stretch = currentFontSetVariation.stretches[0];
    }

    if (currentFontSetVariation.styles.indexOf(selectorStyle) > 0) {
      expectedProperties.style = selectorStyle;
    } else {
      expectedProperties.style = currentFontSetVariation.styles[0];
    }

    if (currentFontSetVariation.weights.indexOf(selectorWeight) > 0) {
      expectedProperties.weight = selectorWeight;
    } else {
      expectedProperties.weight = currentFontSetVariation.weights[0];
    }

    return expectedProperties;
  }

  function logPassFailResult(selector, usedFontName) {
    var actualStretchStyleWeight =
      /CSSMatchingTest (\w*) (\w*) (.*)/.exec(usedFontName);
    var actualStretch, actualStyle, actualWeight;
    if (actualStretchStyleWeight && actualStretchStyleWeight.length > 3) {
      actualStretch = actualStretchStyleWeight[1].toLowerCase();
      actualStyle = actualStretchStyleWeight[2].toLowerCase();
      // Font names have human readable weight description,
      // we need to convert.
      actualWeight = weights[
        weightsHumanReadable.indexOf(actualStretchStyleWeight[3])];
    } else {
      actualStretch = usedFontName;
      actualStyle = "";
      actualWeight = "";
    }

    var expectedProperties = cssStyleMatchingExpectationForSelector(
      selector);

    testRunner.log("Expected: " +
      expectedProperties.stretch + " " +
      expectedProperties.style + " " +
      expectedProperties.weight);
    testRunner.log("Actual: " + actualStretch + " " +
      actualStyle + " " +
      actualWeight);

    if (actualStretch != expectedProperties.stretch ||
      actualStyle != expectedProperties.style ||
      actualWeight != expectedProperties.weight) {
      testRunner.log("FAIL\n");
    } else {
      testRunner.log("PASS\n");
    }
  }
})