<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>SVG size test</title>
    <style>
      body {
        margin: 30px;
        font-family: sans-serif;
      }

      #tests {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        grid-gap: 30px;
      }

      #tests div:hover {
        opacity: 1 !important;
      }

      #tests p {
        text-decoration: underline dotted black;
      }

      svg {
        background: lightgrey;
      }
    </style>
  </head>
  <body>
    <h1>SVG size test</h1>

    <p>The grey boxes below are <code>&lt;svg&gt;</code> elements.</p>
    <p>All SVGs in each row should have the exact same size.</p>
    <p>Each row has its own viewBox width and height: <code>viewBox="0 0 width height"</code>.</p>
    <p>Each column has its own width/height styling. For example, <code>style="width: 200px; height: auto;"</code>.</p>
    <p>The first column has both an explicit widht and an explicit height, so there's not much that can go wrong there. It acts as a reference.</p>
    <p>The first row has integer viewBox width and height. Firefox then sizes all SVGs correctly.</p>
    <p>The remaining rows have at least one non-integer viewBox width and height. Firefox then sizes the SVGs a bit wrong.</p>
    <p>Chrome, Safari and Edge seem to pass all tests.</p>

    <p id="summary"></p>

    <div id="tests"></div>

    <script>
      const testsElement = document.getElementById("tests");
      const summaryElement = document.getElementById("summary");

      // Turn for instance `2.3` into `230` (px). Round to avoid floating point
      // issues.
      const scale = (number) => Math.round(100 * number);

      const widths = [2, 2.3, 2.5, 2.8];
      const heights = [3, 3.3, 3.5, 3.8];

      let numPassed = 0;
      let numFailed = 0;

      for (const width of widths) {
        for (const height of heights) {
          const variations = [
            {width, height},
            {width: "auto", height},
            {width, height: "auto"},
            {width: "auto", height: "auto"},
          ];

          for (const variation of variations) {
            const cellElement = document.createElement("div");

            const titleElement = document.createElement("h2");
            titleElement.appendChild(makeTitle(width, height, variation));

            const sizeElement = document.createElement("p");

            const svgWrapperElement = document.createElement("div");
            svgWrapperElement.style.width =
              variation.width === "auto" && variation.height === "auto"
                ? `${scale(width)}px`
                : "auto";

            const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            svgElement.setAttribute("viewBox", `0 0 ${width} ${height}`);
            svgElement.style.width =
              typeof variation.width === "number"
                ? `${scale(variation.width)}px`
                : variation.width;
            svgElement.style.height =
              typeof variation.height === "number"
                ? `${scale(variation.height)}px`
                : variation.height;

            svgWrapperElement.appendChild(svgElement);

            cellElement.appendChild(titleElement);
            cellElement.appendChild(sizeElement);
            cellElement.appendChild(svgWrapperElement);

            testsElement.appendChild(cellElement);

            const rect = svgElement.getBoundingClientRect();
            const actual = {
              width: Math.round(rect.width),
              height: Math.round(rect.height),
            };
            const expected = {
              width: scale(width),
              height: scale(height),
            };
            const passed =
              actual.width === expected.width &&
              actual.height === expected.height;

            const icon = passed ? "✔" : "✘";
            const iconText = passed ? "PASS" : "FAIL";
            const expectedText = passed ? "" : `\nExpected size: ${expected.width}x${expected.height}`;
            sizeElement.textContent = `${icon} ${actual.width}x${actual.height}`;
            sizeElement.title = `${iconText}. Actual size, as measured by element.getBoundingClientRect().${expectedText}`;
            sizeElement.style.color = passed ? "lime" : "red";
            sizeElement.style.fontWeight = passed ? "normal" : "bold";

            cellElement.style.opacity = passed ? 0.5 : 1;

            if (passed) {
              numPassed++;
            } else {
              numFailed++;
            }
          }
        }
      }

      const numTotal = numPassed + numFailed;
      const passed = numFailed === 0;
      const icon = passed ? "✔" : "✘";
      summaryElement.textContent = `${icon} ${numPassed}/${numTotal} tests passed.`;
      summaryElement.style.color = passed ? "lime" : "red";
      summaryElement.style.fontWeight = "bold";

      function makeTitle(width, height, variation) {
        const fragment = document.createDocumentFragment();

        const first = document.createElement("abbr");
        first.textContent = `${width}/${height}`;
        first.title = `SVG viewBox width/height: viewBox="0 0 ${width} ${height}"`;

        const separator = document.createTextNode(" | ");

        const second = document.createElement("abbr");

        const widthString = typeof variation.width === "number" ? "px" : variation.width;
        const heightString = typeof variation.height === "number" ? "px" : variation.height;
        second.textContent = `${widthString}/${heightString}`;

        const widthExplanation =
          typeof variation.width === "number"
            ? "explicit width (px)"
            : `${variation.width} width`
        const heightExplanation =
          typeof variation.height === "number"
            ? "explicit height (px)"
            : `${variation.height} height`
        second.title = `${widthExplanation}, ${heightExplanation}`;

        fragment.appendChild(first);
        fragment.appendChild(separator);
        fragment.appendChild(second);

        return fragment;
      }
    </script>
  </body>
</html>