18F/web-design-standards

View on GitHub
packages/usa-in-page-navigation/src/test/in-page-navigation.spec.js

Summary

Maintainability
B
6 hrs
Test Coverage
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const sinon = require("sinon");
const behavior = require("../index");

const HIDE_MAX_WIDTH = 639;
const OFFSET_PER_SECTION = 100;
const TEMPLATE = fs.readFileSync(path.join(__dirname, "/template.html"));
const STYLES = fs.readFileSync(
  `${__dirname}/../../../../dist/css/uswds.min.css`
);
const THE_NAV = ".usa-in-page-nav";
const PRIMARY_CONTENT_SELECTOR =
  ".usa-in-page-nav-container .usa-in-page-nav .usa-in-page-nav__list";

/**
 * Resize the window's width, then dispatch a 'resize' event
 *
 * @param {number} width
 */
function resizeTo(width) {
  if (width !== window.innerWidth) {
    window.innerWidth = width;
    window.dispatchEvent(new window.Event("resize"));
  }
}

const assertHidden = (el, hidden) => {
  assert.strictEqual(
    window.getComputedStyle(el).display === "none",
    hidden,
    `not hidden: ${el.nodeName} (${el.className})`
  );
};

const tests = [
  { name: "document.body", selector: () => document.body },
  {
    name: "in page nav",
    selector: () => document.querySelector(".usa-in-page-nav"),
  },
];

tests.forEach(({ name, selector: containerSelector }) => {
  describe(`in-page navigation initialized at ${name}`, () => {
    const { body } = document;
    document.head.insertAdjacentHTML("beforeend", `<style>${STYLES}</style>`);

    let theNav;
    let theList;
    let originalOffsetTop;

    before(() => {
      originalOffsetTop = Object.getOwnPropertyDescriptor(
        HTMLElement.prototype,
        "offsetTop"
      );
      Object.defineProperty(HTMLElement.prototype, "offsetTop", {
        get() {
          // Since JSDOM doesn't emulate positions, create a fake offset using
          // the heading's index to be used to test scrolling behavior.
          const heading = this.closest("h2,h3");

          let index = 0;
          let sibling = heading;
          while (true) {
            sibling = sibling.previousElementSibling;
            if (sibling) {
              index += 1;
            } else {
              break;
            }
          }

          return index * OFFSET_PER_SECTION;
        },
      });
      const observe = sinon.spy();
      const mockIntersectionObserver = sinon.stub().returns({ observe });
      window.IntersectionObserver = mockIntersectionObserver;
      sinon.stub(window, "scroll");
    });

    beforeEach(() => {
      body.innerHTML = TEMPLATE;

      behavior.on(containerSelector());

      theNav = document.querySelector(THE_NAV);
      theList = document.querySelector(PRIMARY_CONTENT_SELECTOR);

      window.innerWidth = 1024;
    });

    afterEach(() => {
      sinon.resetHistory();
      behavior.off(containerSelector(body));
      body.innerHTML = "";
      window.location.hash = "";
    });

    after(() => {
      Object.defineProperty(
        HTMLElement.prototype,
        "offsetTop",
        originalOffsetTop
      );
      sinon.restore();
    });

    it("defines a max width", () => {
      assert(typeof HIDE_MAX_WIDTH === "number", "no value defined");
    });

    it("hides at small screens", () => {
      resizeTo(400);
      assertHidden(theNav, true);
    });

    it("show on larger screens", () => {
      resizeTo(400);
      resizeTo(1024);
      assertHidden(theList, false);
    });

    it("assigns id to section headings", () => {
      // Tests that new anchor children are created in the fixture template in
      // the expected locations.
      const ok = [
        "h2 > .usa-anchor#section-1",
        "h2 ~ h3 > .usa-anchor#section-1-1",
        "h2 ~ h3 ~ h3 > .usa-anchor#section-1-2-2",
        "h2 ~ h3 ~ h3 ~ h3 > .usa-anchor#section-1-3",
        "h2 ~ h3 ~ h3 ~ h3 ~ h3 > .usa-anchor[id='1-4-section-1-4']",
      ].every((selector) => document.querySelector(selector));

      assert(ok);
    });

    it("scrolls to section", () => {
      const firstLink = theNav.querySelector("a[href='#section-1']");
      firstLink.click();

      assert(window.scroll.calledOnceWith(sinon.match({ top: 80 })));
    });

    it("updates url when scrolling to section", () => {
      // Activate by click
      const firstLink = theNav.querySelector("a[href='#section-1']");
      firstLink.click();

      assert.equal(window.location.hash, "#section-1");

      // Activate by Enter press
      const secondLink = theNav.querySelector("a[href='#section-1-1']");
      const event = new KeyboardEvent("keydown", {
        bubbles: true,
        key: "Enter",
        keyCode: 13,
      });
      secondLink.dispatchEvent(event);

      assert.equal(window.location.hash, "#section-1-1");
    });

    it("does not scroll to section on initialization", () => {
      assert.equal(window.scroll.called, false);
    });

    it("handles headings starting with a number", () => {
      const firstLink = theNav.querySelector("a[href='#1-4-section-1-4']");
      firstLink.click();

      assert(window.scroll.calledOnceWith(sinon.match({ top: 880 })));
    });

    context("with initial hash URL", () => {
      before(() => {
        window.location.hash = "#section-1";
      });

      it("scrolls to section on initialization", () => {
        assert(window.scroll.calledOnceWith(sinon.match({ top: 80 })));
      });
    });
  });
});