WeAreGenki/minna-ui

View on GitHub
components/navbar/src/__tests__/Navbar.test.ts

Summary

Maintainability
A
0 mins
Test Coverage
// TODO: Integration/UI tests using puppeteer
//  ↳ Responsive CSS

/* eslint-disable @typescript-eslint/no-non-null-assertion */

import { tick } from 'svelte';
import Navbar from '../Navbar.svelte';
import UseSlot from './__fixtures__/UseSlot.svelte';

const items = [
  { text: 'Page One', url: 'page-one' },
  { text: 'Page Two', url: 'page-two' },
  { text: 'About Us', url: 'about' },
];

describe('Navbar component', () => {
  it('renders correctly with required props set', () => {
    expect.assertions(4);
    const target = document.createElement('div');
    const component = new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    expect(Array.isArray(component.items)).toBe(true);
    expect(component.items).not.toHaveLength(0);
    const navbar = target.querySelector('.navbar')!;
    expect(navbar.getAttribute('navbar-active')).toBeNull();
    expect(target.innerHTML).toMatchSnapshot();
  });

  it("doesn't log errors or warnings with required props", () => {
    expect.assertions(2);
    const spy1 = jest.spyOn(console, 'error');
    const spy2 = jest.spyOn(console, 'warn');
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    expect(spy1).not.toHaveBeenCalled();
    expect(spy2).not.toHaveBeenCalled();
    spy1.mockRestore();
    spy2.mockRestore();
  });

  it('adds class if page is scrolled', async () => {
    expect.assertions(2);
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    expect(target.querySelector('.navbar-active')).toBeNull();
    // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
    // @ts-ignore - We know we're writing to a read only property
    window.pageYOffset = 50;
    const event = new UIEvent('scroll');
    window.dispatchEvent(event);
    await tick();
    expect(target.querySelector('.navbar-active')).not.toBeNull();
  });

  it('opens menu on button click', async () => {
    expect.assertions(2);
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    const navbar1 = target.querySelector('.navbar-active');
    expect(navbar1).toBeNull();
    const button = target.querySelector<HTMLButtonElement>('.navbar-button')!;
    button.click();
    await tick();
    const navbar2 = target.querySelector('.navbar-active');
    expect(navbar2).not.toBeNull();
  });

  it('closes menu on document click', async () => {
    expect.assertions(2);
    jest.useFakeTimers();
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    const button = target.querySelector<HTMLButtonElement>('.navbar-button')!;
    button.click();
    await tick();
    jest.runAllTimers(); // For component setTimeout
    const navbar1 = target.querySelector('.navbar-active');
    expect(navbar1).not.toBeNull();
    const event = new MouseEvent('click');
    document.dispatchEvent(event);
    await tick();
    const navbar2 = target.querySelector('.navbar-active');
    expect(navbar2).toBeNull();
  });

  it('attaches event listener on menu open but not close', async () => {
    expect.assertions(3);
    jest.useFakeTimers();
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    const spy = jest.spyOn(document, 'addEventListener');
    const button = target.querySelector<HTMLButtonElement>('.navbar-button')!;
    button.click();
    jest.runAllTimers();
    expect(spy).toHaveBeenCalledTimes(1);
    spy.mockReset();
    const event = new MouseEvent('click');
    document.dispatchEvent(event);
    await tick();
    jest.runAllTimers();
    const navbar = target.querySelector('.navbar-active');
    expect(navbar).toBeNull();
    expect(spy).not.toHaveBeenCalled();
    spy.mockClear();
    spy.mockRestore();
  });

  it("doesn't attach extra event listeners on multiple button clicks", async () => {
    expect.assertions(3);
    jest.useFakeTimers();
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    const spy = jest.spyOn(document, 'addEventListener');
    spy.mockClear();
    const active1 = target.querySelector('.navbar-active');
    expect(active1).toBeNull();
    const button = target.querySelector<HTMLButtonElement>('.navbar-button')!;
    button.click();
    await tick();
    jest.runAllTimers();
    button.click();
    await tick();
    jest.runAllTimers();
    const active2 = target.querySelector('.navbar-active');
    expect(active2).not.toBeNull();
    button.click();
    await tick();
    jest.runAllTimers();
    expect(spy).toHaveBeenCalledTimes(1);
    spy.mockRestore();
  });

  it('shows correct icon and class when toggled', async () => {
    expect.assertions(4);
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    const icon1 = target.querySelector<SVGUseElement>('.navbar-icon > use')!;
    const navbarLinks = target.querySelector('.navbar-links')!;
    expect(icon1.getAttribute('xlink:href')).toBe('#menu');
    expect(navbarLinks.classList.contains('df')).toBe(false);
    const button = target.querySelector<HTMLButtonElement>('.navbar-button')!;
    button.click();
    await tick();
    const icon2 = target.querySelector<SVGUseElement>('.navbar-icon > use')!;
    expect(icon2.getAttribute('xlink:href')).toBe('#x');
    expect(navbarLinks.classList.contains('df')).toBe(true);
  });

  it('adds aria-current attribute to active menu item', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    new Navbar({
      props: {
        current: 'page-two',
        items,
      },
      target,
    });
    const linkPageTwo = target.querySelector('[href="page-two"]')!;
    const linkPageOne = target.querySelector('[href="page-one"]')!;
    expect(linkPageTwo.getAttribute('aria-current')).toBe('page');
    expect(linkPageOne.getAttribute('aria-current')).toBeNull();
  });

  it('can dynamically add menu items', () => {
    expect.assertions(3);
    const target = document.createElement('div');
    const component = new Navbar({
      props: {
        current: '/',
        items,
      },
      target,
    });
    expect(component.items).toHaveLength(3);
    component.$set({
      items: [...items, { name: 'Page New', url: 'page-new' }],
    });
    expect(component.items).toHaveLength(4);
    expect(target.innerHTML).toMatchSnapshot();
  });

  it('renders custom markup when slot content is used', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    new UseSlot({
      props: {
        current: '/',
        items,
      },
      target,
    });
    const logo = target.querySelector('.navbar-logo-link')!;
    expect(logo.textContent).toBe('Custom Slot Content');
    expect(target.innerHTML).toMatchSnapshot();
  });
});