18F/identity-idp

View on GitHub
app/javascript/packages/manageable-authenticator/manageable-authenticator-element.spec.ts

Summary

Maintainability
A
0 mins
Test Coverage
import quibble from 'quibble';
import { screen, waitFor } from '@testing-library/dom';
import baseUserEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import type { SetupServer } from 'msw/node';
import type { SinonStub } from 'sinon';
import { useSandbox } from '@18f/identity-test-helpers';

describe('ManageableAuthenticatorElement', () => {
  const sandbox = useSandbox({ useFakeTimers: true });
  const { clock } = sandbox;
  const userEvent = baseUserEvent.setup({ advanceTimers: clock.tick });
  let forceSubmit: SinonStub;
  let server: SetupServer;

  function createElement() {
    document.body.innerHTML = `
      <lg-manageable-authenticator
        api-url="${window.location.origin}/api/manage"
        configuration-name="configuration-name"
        unique-id="configuration-123"
        reauthenticate-at="2023-12-07T00:10:00.000Z"
        reauthentication-url="#reauthenticate"
      >
        <script type="application/json" class="manageable-authenticator__strings">
          {"renamed":"Renamed","delete_confirm":"Are you sure?","deleted":"Deleted"}
        </script>
        <div class="manageable-authenticator__edit" tabindex="-1" role="group" aria-labelledby="manageable-authenticator-manage-accessible-label-configuration-123">
          <div class="usa-alert manageable-authenticator__alert" tabindex="-1" role="status">
            <div class="usa-alert__body">
              <p class="usa-alert__text"></p>
            </div>
          </div>
          <form class="manageable-authenticator__rename">
            <input class="manageable-authenticator__rename-input" aria-label="Nickname" value="configuration-name">
            <lg-spinner-button class="manageable-authenticator__save-rename-button" long-wait-duration-ms="Infinity">
              <button name="button" type="submit" class="usa-button">
                <span class="spinner-button__content">Save</span>
                <span class="spinner-dots spinner-dots--centered" aria-hidden="true">
                  <span class="spinner-dots__dot"></span>
                  <span class="spinner-dots__dot"></span>
                  <span class="spinner-dots__dot"></span>
                </span>
              </button>
              <div role="status" data-message="Saving…" class="spinner-button__action-message"></div>
            </lg-spinner-button>
            <button name="button" type="button" class="manageable-authenticator__cancel-rename-button">Cancel</button>
          </div>
        </form>
        <div class="manageable-authenticator__details">
          <span class="usa-sr-only">Nickname:</span>
          <strong class="manageable-authenticator__name manageable-authenticator__details-name">
            configuration-name
          </strong>
          <button name="button" type="button" class="manageable-authenticator__rename-button">Rename</button>
          <lg-spinner-button class="manageable-authenticator__delete-button" long-wait-duration-ms="Infinity">
            <button name="button" type="button" class="usa-button">
              <span class="spinner-button__content">Delete</span>
              <span class="spinner-dots spinner-dots--centered" aria-hidden="true">
                <span class="spinner-dots__dot"></span>
                <span class="spinner-dots__dot"></span>
                <span class="spinner-dots__dot"></span>
              </span>
            </button>
            <div role="status" data-message="Deleting…" class="spinner-button__action-message"></div>
          </lg-spinner-button>
          <button name="button" type="button" class="manageable-authenticator__done-button">Done</button>
        </div>
        <div class="manageable-authenticator__summary">
          <div class="manageable-authenticator__name manageable-authenticator__summary-name">configuration-name</div>
          <div class="manageable-authenticator__actions">
            <button name="button" type="button" class="manageable-authenticator__manage-button">
              <span aria-hidden="true">Manage</span>
              <span class="usa-sr-only" id="manageable-authenticator-manage-accessible-label-configuration-123">
                Manage <span class="manageable-authenticator__name">configuration-name</span>
              </span>
            </button>
          </div>
        </div>
      </lg-manageable-authenticator>
    `;

    return document.body.querySelector('lg-manageable-authenticator')!;
  }

  before(async () => {
    forceSubmit = sandbox.stub();
    quibble('@18f/identity-url', { forceSubmit });
    await Promise.all([
      import('./manageable-authenticator-element'),
      import('@18f/identity-spinner-button/spinner-button-element'),
    ]);
    server = setupServer();
    server.listen();
  });

  beforeEach(() => {
    sandbox.clock.setSystemTime(new Date('2023-12-07T00:00:00Z'));
    server.resetHandlers();
  });

  after(() => {
    server.close();
  });

  it('shows initial manage state', () => {
    const element = createElement();

    expect(element.classList.contains('manageable-authenticator--editing')).to.be.false();
  });

  context('when selected after reauthentication', () => {
    beforeEach(() => {
      jsdom.reconfigure({ url: 'http://example.test/?manage_authenticator=configuration-123' });
    });

    it('shows initial manage state', () => {
      const initialHistoryLength = window.history.length;
      sandbox.spy(HTMLElement.prototype, 'scrollIntoView');
      const element = createElement();

      expect(element.classList.contains('manageable-authenticator--editing')).to.be.true();
      expect(window.location.href).to.equal('http://example.test/');
      expect(window.history).to.have.lengthOf(initialHistoryLength);
      expect(HTMLElement.prototype.scrollIntoView).to.have.been.called();
    });
  });

  describe('clicking manage', () => {
    it('toggles edit details', async () => {
      const element = createElement();

      await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));

      const detailPanel = screen.getByRole('group');

      await waitFor(() =>
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
      );
      expect(document.activeElement).to.equal(detailPanel);
    });

    context('with reauthentication required', () => {
      beforeEach(() => {
        sandbox.clock.setSystemTime(new Date('2023-12-07T15:00:00Z'));
      });

      it('redirects the user to reauthenticate', async () => {
        createElement();

        await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));

        await expect(forceSubmit).to.eventually.be.calledWith('#reauthenticate');
      });
    });
  });

  describe('viewing manage details', () => {
    it('cancels and returns focus to manage button when clicking done', async () => {
      const element = createElement();

      await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
      await waitFor(() =>
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
      );

      await userEvent.click(screen.getByRole('button', { name: 'Done' }));
      expect(element.classList.contains('manageable-authenticator--editing')).to.be.false();
      expect(document.activeElement).to.equal(
        screen.getByRole('button', { name: 'Manage configuration-name' }),
      );
    });

    it('cancels and returns focus to manage button when pressing escape', async () => {
      const element = createElement();

      await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
      await waitFor(() =>
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
      );

      await userEvent.keyboard('{Escape}');
      expect(element.classList.contains('manageable-authenticator--editing')).to.be.false();
      expect(document.activeElement).to.equal(
        screen.getByRole('button', { name: 'Manage configuration-name' }),
      );
    });
  });

  describe('renaming', () => {
    it('focuses the input at the end of the current name', async () => {
      const element = createElement();

      await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
      await waitFor(() =>
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
      );

      await userEvent.click(screen.getByRole('button', { name: 'Rename' }));
      const renameInput = screen.getByRole<HTMLInputElement>('textbox', { name: 'Nickname' });
      expect(document.activeElement).to.equal(renameInput);

      await userEvent.keyboard('appended');
      expect(renameInput.value).to.equal('configuration-nameappended');
    });

    it('cancels (resets edit state) and returns focus to manage button when pressing escape', async () => {
      const element = createElement();

      await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
      await waitFor(() =>
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
      );

      await userEvent.click(screen.getByRole('button', { name: 'Rename' }));
      expect(element.classList.contains('manageable-authenticator--renaming')).to.be.true();

      await userEvent.keyboard('{Escape}');
      expect(element.classList.contains('manageable-authenticator--editing')).to.be.false();
      expect(element.classList.contains('manageable-authenticator--renaming')).to.be.false();
      expect(document.activeElement).to.equal(
        screen.getByRole('button', { name: 'Manage configuration-name' }),
      );
    });

    context('successful response from server when submitting rename', () => {
      beforeEach(() => {
        server.use(http.put('/api/manage', () => HttpResponse.json({ success: true })));
      });

      it('returns the user to summary details with new name for successful save', async () => {
        const element = createElement();

        await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
        await waitFor(() =>
          expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
        );

        await userEvent.click(screen.getByRole('button', { name: 'Rename' }));
        await userEvent.keyboard('-new');
        const saveButton = screen.getByRole('button', { name: 'Save' });

        // Check for spinning while saving
        await userEvent.click(saveButton);
        expect(saveButton.closest('.spinner-button--spinner-active')).to.exist();

        // Change for ARIA live region content update
        const alert = screen
          .getAllByRole('status')
          .find((candidate) => !candidate.closest('lg-spinner-button'))!;
        expect(alert.textContent!.trim()).to.be.empty();
        await waitFor(() => expect(alert.textContent!.trim()).to.equal('Renamed'));
        expect(alert.classList.contains('usa-alert--success')).to.be.true();
        expect(alert.classList.contains('usa-alert--error')).to.be.false();

        // Check return to details
        expect(element.classList.contains('manageable-authenticator--renaming')).to.be.false();
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true();

        // Check for focus target
        const detailPanel = screen.getByRole('group', { name: 'Manage configuration-name-new' });
        expect(document.activeElement).to.equal(detailPanel);

        // Check for new name
        expect(screen.getAllByText('configuration-name-new')).not.to.be.empty();

        // Check for spinner button reset
        await userEvent.click(screen.getByRole('button', { name: 'Rename' }));
        expect(saveButton.closest('.spinner-button--spinner-active')).not.to.exist();
      });
    });

    context('failed response from server when submitting rename', () => {
      beforeEach(() => {
        server.use(
          http.put('/api/manage', () => HttpResponse.json({ error: 'Uh oh!' }, { status: 400 })),
        );
      });

      it('keeps the user on the rename panel and displays the received error', async () => {
        const element = createElement();

        await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
        await waitFor(() =>
          expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
        );

        await userEvent.click(screen.getByRole('button', { name: 'Rename' }));
        await userEvent.keyboard('-new');
        const saveButton = screen.getByRole('button', { name: 'Save' });
        await userEvent.click(saveButton);

        // Change for ARIA live region content update
        const alert = screen
          .getAllByRole('status')
          .find((candidate) => !candidate.closest('lg-spinner-button'))!;
        expect(alert.textContent!.trim()).to.be.empty();
        await waitFor(() => expect(alert.textContent!.trim()).to.equal('Uh oh!'));
        expect(alert.classList.contains('usa-alert--success')).to.be.false();
        expect(alert.classList.contains('usa-alert--error')).to.be.true();

        // Check still renaming
        expect(element.classList.contains('manageable-authenticator--renaming')).to.be.true();
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true();

        // Check for focus target
        expect(document.activeElement).to.equal(saveButton);

        // Check that new name was not assigned
        expect(screen.queryAllByText('configuration-name-new')).to.be.empty();

        // Check for spinner button reset
        expect(saveButton.closest('.spinner-button--spinner-active')).not.to.exist();
      });
    });

    context('with reauthentication required', () => {
      it('redirects the user to reauthenticate', async () => {
        const element = createElement();

        await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
        await waitFor(() =>
          expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
        );

        await userEvent.click(screen.getByRole('button', { name: 'Rename' }));
        await userEvent.keyboard('-new');

        sandbox.clock.setSystemTime(new Date('2023-12-07T15:00:00Z'));
        await userEvent.click(screen.getByRole('button', { name: 'Save' }));

        await expect(forceSubmit).to.eventually.be.calledWith('#reauthenticate');
      });
    });
  });

  describe('deleting', () => {
    it('prompts the user and resets button if they cancel', async () => {
      const element = createElement();

      await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
      await waitFor(() =>
        expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
      );

      sandbox.stub(window, 'confirm').returns(false);
      const deleteButton = screen.getByRole('button', { name: 'Delete' });
      await userEvent.click(deleteButton);

      expect(document.activeElement).to.equal(deleteButton);
      expect(deleteButton.closest('.spinner-button--spinner-active')).not.to.exist();
    });

    context('successful response from server when deleting', () => {
      beforeEach(() => {
        server.use(http.delete('/api/manage', () => HttpResponse.json({ success: true })));
      });

      it('deletes the authenticator and displays a confirmation message', async () => {
        const element = createElement();

        await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
        await waitFor(() =>
          expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
        );

        sandbox.stub(window, 'confirm').returns(true);
        const deleteButton = screen.getByRole('button', { name: 'Delete' });
        await userEvent.click(deleteButton);

        const alert = screen
          .getAllByRole('status')
          .find((candidate) => !candidate.closest('lg-spinner-button'))!;
        expect(alert.textContent!.trim()).to.be.empty();
        await waitFor(() => expect(alert.textContent!.trim()).to.equal('Deleted'));

        expect(alert.classList.contains('usa-alert--success')).to.be.true();
        expect(alert.classList.contains('usa-alert--error')).to.be.false();
        expect(document.activeElement).to.equal(alert);
        expect(element.classList.contains('manageable-authenticator--deleted')).to.be.true();
      });
    });

    context('failed response from server when deleting', () => {
      beforeEach(() => {
        server.use(
          http.delete('/api/manage', () => HttpResponse.json({ error: 'Uh oh!' }, { status: 400 })),
        );
      });

      it('displays the error message', async () => {
        const element = createElement();

        await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
        await waitFor(() =>
          expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
        );

        sandbox.stub(window, 'confirm').returns(true);
        const deleteButton = screen.getByRole('button', { name: 'Delete' });
        await userEvent.click(deleteButton);

        const alert = screen
          .getAllByRole('status')
          .find((candidate) => !candidate.closest('lg-spinner-button'))!;
        expect(alert.textContent!.trim()).to.be.empty();
        await waitFor(() => expect(alert.textContent!.trim()).to.equal('Uh oh!'));

        expect(alert.classList.contains('usa-alert--success')).to.be.false();
        expect(alert.classList.contains('usa-alert--error')).to.be.true();
        expect(document.activeElement).to.equal(deleteButton);
        expect(deleteButton.closest('.spinner-button--spinner-active')).not.to.exist();
        expect(element.classList.contains('manageable-authenticator--deleted')).to.be.false();
      });
    });

    context('with reauthentication required', () => {
      it('redirects the user to reauthenticate', async () => {
        const element = createElement();

        await userEvent.click(screen.getByRole('button', { name: 'Manage configuration-name' }));
        await waitFor(() =>
          expect(element.classList.contains('manageable-authenticator--editing')).to.be.true(),
        );

        sandbox.stub(window, 'confirm').returns(true);
        sandbox.clock.setSystemTime(new Date('2023-12-07T15:00:00Z'));
        await userEvent.click(screen.getByRole('button', { name: 'Delete' }));

        await expect(forceSubmit).to.eventually.be.calledWith('#reauthenticate');
      });
    });
  });
});