Back to Repositories

Testing Top Languages Card Rendering in github-readme-stats

This comprehensive test suite validates the functionality of the Top Languages card component in github-readme-stats, covering layout rendering, language calculations, and visual customization options.

Test Coverage Overview

The test suite provides thorough coverage of the Top Languages card functionality, including language percentage calculations, layout options (normal, compact, donut, pie), and size computations. Key test areas include language filtering, custom styling, internationalization, and edge cases like single language displays.

  • Language calculations and trimming
  • Layout-specific rendering tests
  • Theme and color customization
  • Responsive sizing and dimensions

Implementation Analysis

The implementation uses Jest and Testing Library for comprehensive unit testing. The approach focuses on DOM manipulation verification and mathematical accuracy for various chart layouts. Notable patterns include helper function isolation, SVG path validation, and layout-specific test organization.

  • DOM-based testing with @testing-library/dom
  • Mathematical helper function validation
  • SVG path and attribute verification

Technical Details

Testing Tools & Setup:

  • Jest as the main test runner
  • @testing-library/dom for DOM queries
  • @testing-library/jest-dom for custom assertions
  • CSS-to-object parser for style validation
  • Custom SVG path calculation helpers

Best Practices Demonstrated

The test suite exemplifies solid testing practices through comprehensive coverage of component functionality, clear test organization, and thorough edge case handling. Notable practices include isolated helper function testing, detailed layout verification, and robust error scenario coverage.

  • Modular test organization
  • Comprehensive edge case handling
  • Detailed visual verification
  • Mathematical accuracy validation

anuraghazra/github-readme-stats

tests/renderTopLanguagesCard.test.js

            
import { queryAllByTestId, queryByTestId } from "@testing-library/dom";
import { cssToObject } from "@uppercod/css-to-object";
import {
  getLongestLang,
  degreesToRadians,
  radiansToDegrees,
  polarToCartesian,
  cartesianToPolar,
  getCircleLength,
  calculateCompactLayoutHeight,
  calculateNormalLayoutHeight,
  calculateDonutLayoutHeight,
  calculateDonutVerticalLayoutHeight,
  calculatePieLayoutHeight,
  donutCenterTranslation,
  trimTopLanguages,
  renderTopLanguages,
  MIN_CARD_WIDTH,
  getDefaultLanguagesCountByLayout,
} from "../src/cards/top-languages-card.js";
import { expect, it, describe } from "@jest/globals";

// adds special assertions like toHaveTextContent
import "@testing-library/jest-dom";

import { themes } from "../themes/index.js";

const langs = {
  HTML: {
    color: "#0f0",
    name: "HTML",
    size: 200,
  },
  javascript: {
    color: "#0ff",
    name: "javascript",
    size: 200,
  },
  css: {
    color: "#ff0",
    name: "css",
    size: 100,
  },
};

/**
 * Retrieve number array from SVG path definition string.
 *
 * @param {string} d SVG path definition string.
 * @returns {number[]} Resulting numbers array.
 */
const getNumbersFromSvgPathDefinitionAttribute = (d) => {
  return d
    .split(" ")
    .filter((x) => !isNaN(x))
    .map((x) => parseFloat(x));
};

/**
 * Retrieve the language percentage from the donut chart SVG.
 *
 * @param {string} d The SVG path element.
 * @param {number} centerX The center X coordinate of the donut chart.
 * @param {number} centerY The center Y coordinate of the donut chart.
 * @returns {number} The percentage of the language.
 */
const langPercentFromDonutLayoutSvg = (d, centerX, centerY) => {
  const dTmp = getNumbersFromSvgPathDefinitionAttribute(d);
  const endAngle =
    cartesianToPolar(centerX, centerY, dTmp[0], dTmp[1]).angleInDegrees + 90;
  let startAngle =
    cartesianToPolar(centerX, centerY, dTmp[7], dTmp[8]).angleInDegrees + 90;
  if (startAngle > endAngle) {
    startAngle -= 360;
  }
  return (endAngle - startAngle) / 3.6;
};

/**
 * Calculate language percentage for donut vertical chart SVG.
 *
 * @param {number} partLength Length of current chart part..
 * @param {number} totalCircleLength Total length of circle.
 * @returns {number} Chart part percentage.
 */
const langPercentFromDonutVerticalLayoutSvg = (
  partLength,
  totalCircleLength,
) => {
  return (partLength / totalCircleLength) * 100;
};

/**
 * Retrieve the language percentage from the pie chart SVG.
 *
 * @param {string} d The SVG path element.
 * @param {number} centerX The center X coordinate of the pie chart.
 * @param {number} centerY The center Y coordinate of the pie chart.
 * @returns {number} The percentage of the language.
 */
const langPercentFromPieLayoutSvg = (d, centerX, centerY) => {
  const dTmp = getNumbersFromSvgPathDefinitionAttribute(d);
  const startAngle = cartesianToPolar(
    centerX,
    centerY,
    dTmp[2],
    dTmp[3],
  ).angleInDegrees;
  let endAngle = cartesianToPolar(
    centerX,
    centerY,
    dTmp[9],
    dTmp[10],
  ).angleInDegrees;
  return ((endAngle - startAngle) / 360) * 100;
};

describe("Test renderTopLanguages helper functions", () => {
  it("getLongestLang", () => {
    const langArray = Object.values(langs);
    expect(getLongestLang(langArray)).toBe(langs.javascript);
  });

  it("degreesToRadians", () => {
    expect(degreesToRadians(0)).toBe(0);
    expect(degreesToRadians(90)).toBe(Math.PI / 2);
    expect(degreesToRadians(180)).toBe(Math.PI);
    expect(degreesToRadians(270)).toBe((3 * Math.PI) / 2);
    expect(degreesToRadians(360)).toBe(2 * Math.PI);
  });

  it("radiansToDegrees", () => {
    expect(radiansToDegrees(0)).toBe(0);
    expect(radiansToDegrees(Math.PI / 2)).toBe(90);
    expect(radiansToDegrees(Math.PI)).toBe(180);
    expect(radiansToDegrees((3 * Math.PI) / 2)).toBe(270);
    expect(radiansToDegrees(2 * Math.PI)).toBe(360);
  });

  it("polarToCartesian", () => {
    expect(polarToCartesian(100, 100, 60, 0)).toStrictEqual({ x: 160, y: 100 });
    expect(polarToCartesian(100, 100, 60, 45)).toStrictEqual({
      x: 142.42640687119285,
      y: 142.42640687119285,
    });
    expect(polarToCartesian(100, 100, 60, 90)).toStrictEqual({
      x: 100,
      y: 160,
    });
    expect(polarToCartesian(100, 100, 60, 135)).toStrictEqual({
      x: 57.573593128807154,
      y: 142.42640687119285,
    });
    expect(polarToCartesian(100, 100, 60, 180)).toStrictEqual({
      x: 40,
      y: 100.00000000000001,
    });
    expect(polarToCartesian(100, 100, 60, 225)).toStrictEqual({
      x: 57.57359312880714,
      y: 57.573593128807154,
    });
    expect(polarToCartesian(100, 100, 60, 270)).toStrictEqual({
      x: 99.99999999999999,
      y: 40,
    });
    expect(polarToCartesian(100, 100, 60, 315)).toStrictEqual({
      x: 142.42640687119285,
      y: 57.57359312880714,
    });
    expect(polarToCartesian(100, 100, 60, 360)).toStrictEqual({
      x: 160,
      y: 99.99999999999999,
    });
  });

  it("cartesianToPolar", () => {
    expect(cartesianToPolar(100, 100, 160, 100)).toStrictEqual({
      radius: 60,
      angleInDegrees: 0,
    });
    expect(
      cartesianToPolar(100, 100, 142.42640687119285, 142.42640687119285),
    ).toStrictEqual({ radius: 60.00000000000001, angleInDegrees: 45 });
    expect(cartesianToPolar(100, 100, 100, 160)).toStrictEqual({
      radius: 60,
      angleInDegrees: 90,
    });
    expect(
      cartesianToPolar(100, 100, 57.573593128807154, 142.42640687119285),
    ).toStrictEqual({ radius: 60, angleInDegrees: 135 });
    expect(cartesianToPolar(100, 100, 40, 100.00000000000001)).toStrictEqual({
      radius: 60,
      angleInDegrees: 180,
    });
    expect(
      cartesianToPolar(100, 100, 57.57359312880714, 57.573593128807154),
    ).toStrictEqual({ radius: 60, angleInDegrees: 225 });
    expect(cartesianToPolar(100, 100, 99.99999999999999, 40)).toStrictEqual({
      radius: 60,
      angleInDegrees: 270,
    });
    expect(
      cartesianToPolar(100, 100, 142.42640687119285, 57.57359312880714),
    ).toStrictEqual({ radius: 60.00000000000001, angleInDegrees: 315 });
    expect(cartesianToPolar(100, 100, 160, 99.99999999999999)).toStrictEqual({
      radius: 60,
      angleInDegrees: 360,
    });
  });

  it("calculateCompactLayoutHeight", () => {
    expect(calculateCompactLayoutHeight(0)).toBe(90);
    expect(calculateCompactLayoutHeight(1)).toBe(115);
    expect(calculateCompactLayoutHeight(2)).toBe(115);
    expect(calculateCompactLayoutHeight(3)).toBe(140);
    expect(calculateCompactLayoutHeight(4)).toBe(140);
    expect(calculateCompactLayoutHeight(5)).toBe(165);
    expect(calculateCompactLayoutHeight(6)).toBe(165);
    expect(calculateCompactLayoutHeight(7)).toBe(190);
    expect(calculateCompactLayoutHeight(8)).toBe(190);
    expect(calculateCompactLayoutHeight(9)).toBe(215);
    expect(calculateCompactLayoutHeight(10)).toBe(215);
  });

  it("calculateNormalLayoutHeight", () => {
    expect(calculateNormalLayoutHeight(0)).toBe(85);
    expect(calculateNormalLayoutHeight(1)).toBe(125);
    expect(calculateNormalLayoutHeight(2)).toBe(165);
    expect(calculateNormalLayoutHeight(3)).toBe(205);
    expect(calculateNormalLayoutHeight(4)).toBe(245);
    expect(calculateNormalLayoutHeight(5)).toBe(285);
    expect(calculateNormalLayoutHeight(6)).toBe(325);
    expect(calculateNormalLayoutHeight(7)).toBe(365);
    expect(calculateNormalLayoutHeight(8)).toBe(405);
    expect(calculateNormalLayoutHeight(9)).toBe(445);
    expect(calculateNormalLayoutHeight(10)).toBe(485);
  });

  it("calculateDonutLayoutHeight", () => {
    expect(calculateDonutLayoutHeight(0)).toBe(215);
    expect(calculateDonutLayoutHeight(1)).toBe(215);
    expect(calculateDonutLayoutHeight(2)).toBe(215);
    expect(calculateDonutLayoutHeight(3)).toBe(215);
    expect(calculateDonutLayoutHeight(4)).toBe(215);
    expect(calculateDonutLayoutHeight(5)).toBe(215);
    expect(calculateDonutLayoutHeight(6)).toBe(247);
    expect(calculateDonutLayoutHeight(7)).toBe(279);
    expect(calculateDonutLayoutHeight(8)).toBe(311);
    expect(calculateDonutLayoutHeight(9)).toBe(343);
    expect(calculateDonutLayoutHeight(10)).toBe(375);
  });

  it("calculateDonutVerticalLayoutHeight", () => {
    expect(calculateDonutVerticalLayoutHeight(0)).toBe(300);
    expect(calculateDonutVerticalLayoutHeight(1)).toBe(325);
    expect(calculateDonutVerticalLayoutHeight(2)).toBe(325);
    expect(calculateDonutVerticalLayoutHeight(3)).toBe(350);
    expect(calculateDonutVerticalLayoutHeight(4)).toBe(350);
    expect(calculateDonutVerticalLayoutHeight(5)).toBe(375);
    expect(calculateDonutVerticalLayoutHeight(6)).toBe(375);
    expect(calculateDonutVerticalLayoutHeight(7)).toBe(400);
    expect(calculateDonutVerticalLayoutHeight(8)).toBe(400);
    expect(calculateDonutVerticalLayoutHeight(9)).toBe(425);
    expect(calculateDonutVerticalLayoutHeight(10)).toBe(425);
  });

  it("calculatePieLayoutHeight", () => {
    expect(calculatePieLayoutHeight(0)).toBe(300);
    expect(calculatePieLayoutHeight(1)).toBe(325);
    expect(calculatePieLayoutHeight(2)).toBe(325);
    expect(calculatePieLayoutHeight(3)).toBe(350);
    expect(calculatePieLayoutHeight(4)).toBe(350);
    expect(calculatePieLayoutHeight(5)).toBe(375);
    expect(calculatePieLayoutHeight(6)).toBe(375);
    expect(calculatePieLayoutHeight(7)).toBe(400);
    expect(calculatePieLayoutHeight(8)).toBe(400);
    expect(calculatePieLayoutHeight(9)).toBe(425);
    expect(calculatePieLayoutHeight(10)).toBe(425);
  });

  it("donutCenterTranslation", () => {
    expect(donutCenterTranslation(0)).toBe(-45);
    expect(donutCenterTranslation(1)).toBe(-45);
    expect(donutCenterTranslation(2)).toBe(-45);
    expect(donutCenterTranslation(3)).toBe(-45);
    expect(donutCenterTranslation(4)).toBe(-45);
    expect(donutCenterTranslation(5)).toBe(-45);
    expect(donutCenterTranslation(6)).toBe(-29);
    expect(donutCenterTranslation(7)).toBe(-13);
    expect(donutCenterTranslation(8)).toBe(3);
    expect(donutCenterTranslation(9)).toBe(19);
    expect(donutCenterTranslation(10)).toBe(35);
  });

  it("getCircleLength", () => {
    expect(getCircleLength(20)).toBeCloseTo(125.663);
    expect(getCircleLength(30)).toBeCloseTo(188.495);
    expect(getCircleLength(40)).toBeCloseTo(251.327);
    expect(getCircleLength(50)).toBeCloseTo(314.159);
    expect(getCircleLength(60)).toBeCloseTo(376.991);
    expect(getCircleLength(70)).toBeCloseTo(439.822);
    expect(getCircleLength(80)).toBeCloseTo(502.654);
    expect(getCircleLength(90)).toBeCloseTo(565.486);
    expect(getCircleLength(100)).toBeCloseTo(628.318);
  });

  it("trimTopLanguages", () => {
    expect(trimTopLanguages([])).toStrictEqual({
      langs: [],
      totalLanguageSize: 0,
    });
    expect(trimTopLanguages([langs.javascript])).toStrictEqual({
      langs: [langs.javascript],
      totalLanguageSize: 200,
    });
    expect(trimTopLanguages([langs.javascript, langs.HTML], 5)).toStrictEqual({
      langs: [langs.javascript, langs.HTML],
      totalLanguageSize: 400,
    });
    expect(trimTopLanguages(langs, 5)).toStrictEqual({
      langs: Object.values(langs),
      totalLanguageSize: 500,
    });
    expect(trimTopLanguages(langs, 2)).toStrictEqual({
      langs: Object.values(langs).slice(0, 2),
      totalLanguageSize: 400,
    });
    expect(trimTopLanguages(langs, 5, ["javascript"])).toStrictEqual({
      langs: [langs.HTML, langs.css],
      totalLanguageSize: 300,
    });
  });

  it("getDefaultLanguagesCountByLayout", () => {
    expect(
      getDefaultLanguagesCountByLayout({ layout: "normal" }),
    ).toStrictEqual(5);
    expect(getDefaultLanguagesCountByLayout({})).toStrictEqual(5);
    expect(
      getDefaultLanguagesCountByLayout({ layout: "compact" }),
    ).toStrictEqual(6);
    expect(
      getDefaultLanguagesCountByLayout({ hide_progress: true }),
    ).toStrictEqual(6);
    expect(getDefaultLanguagesCountByLayout({ layout: "donut" })).toStrictEqual(
      5,
    );
    expect(
      getDefaultLanguagesCountByLayout({ layout: "donut-vertical" }),
    ).toStrictEqual(6);
    expect(getDefaultLanguagesCountByLayout({ layout: "pie" })).toStrictEqual(
      6,
    );
  });
});

describe("Test renderTopLanguages", () => {
  it("should render correctly", () => {
    document.body.innerHTML = renderTopLanguages(langs);

    expect(queryByTestId(document.body, "header")).toHaveTextContent(
      "Most Used Languages",
    );

    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML",
    );
    expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
      "javascript",
    );
    expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
      "css",
    );
    expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute(
      "width",
      "40%",
    );
    expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute(
      "width",
      "40%",
    );
    expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute(
      "width",
      "20%",
    );
  });

  it("should hide languages when hide is passed", () => {
    document.body.innerHTML = renderTopLanguages(langs, {
      hide: ["HTML"],
    });
    expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
      "javascript",
    );
    expect(queryAllByTestId(document.body, "lang-name")[1]).toBeInTheDocument(
      "css",
    );
    expect(queryAllByTestId(document.body, "lang-name")[2]).not.toBeDefined();

    // multiple languages passed
    document.body.innerHTML = renderTopLanguages(langs, {
      hide: ["HTML", "css"],
    });
    expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
      "javascript",
    );
    expect(queryAllByTestId(document.body, "lang-name")[1]).not.toBeDefined();
  });

  it("should resize the height correctly depending on langs", () => {
    document.body.innerHTML = renderTopLanguages(langs, {});
    expect(document.querySelector("svg")).toHaveAttribute("height", "205");

    document.body.innerHTML = renderTopLanguages(
      {
        ...langs,
        python: {
          color: "#ff0",
          name: "python",
          size: 100,
        },
      },
      {},
    );
    expect(document.querySelector("svg")).toHaveAttribute("height", "245");
  });

  it("should render with custom width set", () => {
    document.body.innerHTML = renderTopLanguages(langs, {});

    expect(document.querySelector("svg")).toHaveAttribute("width", "300");

    document.body.innerHTML = renderTopLanguages(langs, { card_width: 400 });
    expect(document.querySelector("svg")).toHaveAttribute("width", "400");
  });

  it("should render with min width", () => {
    document.body.innerHTML = renderTopLanguages(langs, { card_width: 190 });

    expect(document.querySelector("svg")).toHaveAttribute(
      "width",
      MIN_CARD_WIDTH.toString(),
    );

    document.body.innerHTML = renderTopLanguages(langs, { card_width: 100 });
    expect(document.querySelector("svg")).toHaveAttribute(
      "width",
      MIN_CARD_WIDTH.toString(),
    );
  });

  it("should render default colors properly", () => {
    document.body.innerHTML = renderTopLanguages(langs);

    const styleTag = document.querySelector("style");
    const stylesObject = cssToObject(styleTag.textContent);

    const headerStyles = stylesObject[":host"][".header "];
    const langNameStyles = stylesObject[":host"][".lang-name "];

    expect(headerStyles.fill.trim()).toBe("#2f80ed");
    expect(langNameStyles.fill.trim()).toBe("#434d58");
    expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
      "fill",
      "#fffefe",
    );
  });

  it("should render custom colors properly", () => {
    const customColors = {
      title_color: "5a0",
      icon_color: "1b998b",
      text_color: "9991",
      bg_color: "252525",
    };

    document.body.innerHTML = renderTopLanguages(langs, { ...customColors });

    const styleTag = document.querySelector("style");
    const stylesObject = cssToObject(styleTag.innerHTML);

    const headerStyles = stylesObject[":host"][".header "];
    const langNameStyles = stylesObject[":host"][".lang-name "];

    expect(headerStyles.fill.trim()).toBe(`#${customColors.title_color}`);
    expect(langNameStyles.fill.trim()).toBe(`#${customColors.text_color}`);
    expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
      "fill",
      "#252525",
    );
  });

  it("should render custom colors with themes", () => {
    document.body.innerHTML = renderTopLanguages(langs, {
      title_color: "5a0",
      theme: "radical",
    });

    const styleTag = document.querySelector("style");
    const stylesObject = cssToObject(styleTag.innerHTML);

    const headerStyles = stylesObject[":host"][".header "];
    const langNameStyles = stylesObject[":host"][".lang-name "];

    expect(headerStyles.fill.trim()).toBe("#5a0");
    expect(langNameStyles.fill.trim()).toBe(`#${themes.radical.text_color}`);
    expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
      "fill",
      `#${themes.radical.bg_color}`,
    );
  });

  it("should render with all the themes", () => {
    Object.keys(themes).forEach((name) => {
      document.body.innerHTML = renderTopLanguages(langs, {
        theme: name,
      });

      const styleTag = document.querySelector("style");
      const stylesObject = cssToObject(styleTag.innerHTML);

      const headerStyles = stylesObject[":host"][".header "];
      const langNameStyles = stylesObject[":host"][".lang-name "];

      expect(headerStyles.fill.trim()).toBe(`#${themes[name].title_color}`);
      expect(langNameStyles.fill.trim()).toBe(`#${themes[name].text_color}`);
      const backgroundElement = queryByTestId(document.body, "card-bg");
      const backgroundElementFill = backgroundElement.getAttribute("fill");
      expect([`#${themes[name].bg_color}`, "url(#gradient)"]).toContain(
        backgroundElementFill,
      );
    });
  });

  it("should render with layout compact", () => {
    document.body.innerHTML = renderTopLanguages(langs, { layout: "compact" });

    expect(queryByTestId(document.body, "header")).toHaveTextContent(
      "Most Used Languages",
    );

    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute(
      "width",
      "100",
    );

    expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
      "javascript 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute(
      "width",
      "100",
    );

    expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
      "css 20.00%",
    );
    expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute(
      "width",
      "50",
    );
  });

  it("should render with layout donut", () => {
    document.body.innerHTML = renderTopLanguages(langs, { layout: "donut" });

    expect(queryByTestId(document.body, "header")).toHaveTextContent(
      "Most Used Languages",
    );

    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute(
      "size",
      "40",
    );
    const d = getNumbersFromSvgPathDefinitionAttribute(
      queryAllByTestId(document.body, "lang-donut")[0].getAttribute("d"),
    );
    const center = { x: d[7], y: d[7] };
    const HTMLLangPercent = langPercentFromDonutLayoutSvg(
      queryAllByTestId(document.body, "lang-donut")[0].getAttribute("d"),
      center.x,
      center.y,
    );
    expect(HTMLLangPercent).toBeCloseTo(40);

    expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
      "javascript 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[1]).toHaveAttribute(
      "size",
      "40",
    );
    const javascriptLangPercent = langPercentFromDonutLayoutSvg(
      queryAllByTestId(document.body, "lang-donut")[1].getAttribute("d"),
      center.x,
      center.y,
    );
    expect(javascriptLangPercent).toBeCloseTo(40);

    expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
      "css 20.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[2]).toHaveAttribute(
      "size",
      "20",
    );
    const cssLangPercent = langPercentFromDonutLayoutSvg(
      queryAllByTestId(document.body, "lang-donut")[2].getAttribute("d"),
      center.x,
      center.y,
    );
    expect(cssLangPercent).toBeCloseTo(20);

    expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100);

    // Should render full donut (circle) if one language is 100%.
    document.body.innerHTML = renderTopLanguages(
      { HTML: langs.HTML },
      { layout: "donut" },
    );
    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML 100.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute(
      "size",
      "100",
    );
    expect(queryAllByTestId(document.body, "lang-donut")).toHaveLength(1);
    expect(queryAllByTestId(document.body, "lang-donut")[0].tagName).toBe(
      "circle",
    );
  });

  it("should render with layout donut vertical", () => {
    document.body.innerHTML = renderTopLanguages(langs, {
      layout: "donut-vertical",
    });

    expect(queryByTestId(document.body, "header")).toHaveTextContent(
      "Most Used Languages",
    );

    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute(
      "size",
      "40",
    );

    const totalCircleLength = queryAllByTestId(
      document.body,
      "lang-donut",
    )[0].getAttribute("stroke-dasharray");

    const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg(
      queryAllByTestId(document.body, "lang-donut")[1].getAttribute(
        "stroke-dashoffset",
      ) -
        queryAllByTestId(document.body, "lang-donut")[0].getAttribute(
          "stroke-dashoffset",
        ),
      totalCircleLength,
    );
    expect(HTMLLangPercent).toBeCloseTo(40);

    expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
      "javascript 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[1]).toHaveAttribute(
      "size",
      "40",
    );
    const javascriptLangPercent = langPercentFromDonutVerticalLayoutSvg(
      queryAllByTestId(document.body, "lang-donut")[2].getAttribute(
        "stroke-dashoffset",
      ) -
        queryAllByTestId(document.body, "lang-donut")[1].getAttribute(
          "stroke-dashoffset",
        ),
      totalCircleLength,
    );
    expect(javascriptLangPercent).toBeCloseTo(40);

    expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
      "css 20.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[2]).toHaveAttribute(
      "size",
      "20",
    );
    const cssLangPercent = langPercentFromDonutVerticalLayoutSvg(
      totalCircleLength -
        queryAllByTestId(document.body, "lang-donut")[2].getAttribute(
          "stroke-dashoffset",
        ),
      totalCircleLength,
    );
    expect(cssLangPercent).toBeCloseTo(20);

    expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100);
  });

  it("should render with layout donut vertical full donut circle of one language is 100%", () => {
    document.body.innerHTML = renderTopLanguages(
      { HTML: langs.HTML },
      { layout: "donut-vertical" },
    );
    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML 100.00%",
    );
    expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute(
      "size",
      "100",
    );
    const totalCircleLength = queryAllByTestId(
      document.body,
      "lang-donut",
    )[0].getAttribute("stroke-dasharray");

    const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg(
      totalCircleLength -
        queryAllByTestId(document.body, "lang-donut")[0].getAttribute(
          "stroke-dashoffset",
        ),
      totalCircleLength,
    );
    expect(HTMLLangPercent).toBeCloseTo(100);
  });

  it("should render with layout pie", () => {
    document.body.innerHTML = renderTopLanguages(langs, { layout: "pie" });

    expect(queryByTestId(document.body, "header")).toHaveTextContent(
      "Most Used Languages",
    );

    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-pie")[0]).toHaveAttribute(
      "size",
      "40",
    );

    const d = getNumbersFromSvgPathDefinitionAttribute(
      queryAllByTestId(document.body, "lang-pie")[0].getAttribute("d"),
    );
    const center = { x: d[0], y: d[1] };
    const HTMLLangPercent = langPercentFromPieLayoutSvg(
      queryAllByTestId(document.body, "lang-pie")[0].getAttribute("d"),
      center.x,
      center.y,
    );
    expect(HTMLLangPercent).toBeCloseTo(40);

    expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
      "javascript 40.00%",
    );
    expect(queryAllByTestId(document.body, "lang-pie")[1]).toHaveAttribute(
      "size",
      "40",
    );
    const javascriptLangPercent = langPercentFromPieLayoutSvg(
      queryAllByTestId(document.body, "lang-pie")[1].getAttribute("d"),
      center.x,
      center.y,
    );
    expect(javascriptLangPercent).toBeCloseTo(40);

    expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
      "css 20.00%",
    );
    expect(queryAllByTestId(document.body, "lang-pie")[2]).toHaveAttribute(
      "size",
      "20",
    );
    const cssLangPercent = langPercentFromPieLayoutSvg(
      queryAllByTestId(document.body, "lang-pie")[2].getAttribute("d"),
      center.x,
      center.y,
    );
    expect(cssLangPercent).toBeCloseTo(20);

    expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100);

    // Should render full pie (circle) if one language is 100%.
    document.body.innerHTML = renderTopLanguages(
      { HTML: langs.HTML },
      { layout: "pie" },
    );
    expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
      "HTML 100.00%",
    );
    expect(queryAllByTestId(document.body, "lang-pie")[0]).toHaveAttribute(
      "size",
      "100",
    );
    expect(queryAllByTestId(document.body, "lang-pie")).toHaveLength(1);
    expect(queryAllByTestId(document.body, "lang-pie")[0].tagName).toBe(
      "circle",
    );
  });

  it("should render a translated title", () => {
    document.body.innerHTML = renderTopLanguages(langs, { locale: "cn" });
    expect(document.getElementsByClassName("header")[0].textContent).toBe(
      "最常用的语言",
    );
  });

  it("should render without rounding", () => {
    document.body.innerHTML = renderTopLanguages(langs, { border_radius: "0" });
    expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
    document.body.innerHTML = renderTopLanguages(langs, {});
    expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
  });

  it("should render langs with specified langs_count", () => {
    const options = {
      langs_count: 1,
    };
    document.body.innerHTML = renderTopLanguages(langs, { ...options });
    expect(queryAllByTestId(document.body, "lang-name").length).toBe(
      options.langs_count,
    );
  });

  it("should render langs with specified langs_count even when hide is set", () => {
    const options = {
      hide: ["HTML"],
      langs_count: 2,
    };
    document.body.innerHTML = renderTopLanguages(langs, { ...options });
    expect(queryAllByTestId(document.body, "lang-name").length).toBe(
      options.langs_count,
    );
  });

  it('should show "No languages data." message instead of empty card when nothing to show', () => {
    document.body.innerHTML = renderTopLanguages({});
    expect(document.querySelector(".stat").textContent).toBe(
      "No languages data.",
    );
  });
});