chromium/chrome/test/data/webui/cr_components/help_bubble/help_bubble_test.ts

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://resources/cr_components/help_bubble/help_bubble.js';

import type {CrButtonElement} from '//resources/cr_elements/cr_button/cr_button.js';
import type {HelpBubbleDismissedEvent, HelpBubbleTimedOutEvent} from 'chrome://resources/cr_components/help_bubble/help_bubble.js';
import {HELP_BUBBLE_DISMISSED_EVENT, HELP_BUBBLE_TIMED_OUT_EVENT, HelpBubbleElement} from 'chrome://resources/cr_components/help_bubble/help_bubble.js';
import type {HelpBubbleButtonParams} from 'chrome://resources/cr_components/help_bubble/help_bubble.mojom-webui.js';
import {HelpBubbleArrowPosition} from 'chrome://resources/cr_components/help_bubble/help_bubble.mojom-webui.js';
import {getTrustedHTML} from 'chrome://resources/js/static_types.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {isVisible, microtasksFinished} from 'chrome://webui-test/test_util.js';

interface WaitForSuccessParams {
  retryIntervalMs: number;
  totalMs: number;
  assertionFn: () => void;
}

let counter = 0;
function getMockId(): string {
  return (++counter).toString();
}

suite('CrComponentsHelpBubbleTest', () => {
  let helpBubble: HelpBubbleElement;

  function getNumButtons() {
    return helpBubble.$.buttons.querySelectorAll('[id*="action-button"').length;
  }

  function getProgressIndicators() {
    return helpBubble.$.progress.querySelectorAll('[class*="progress"]');
  }

  /**
   * Asserts that body element is only visible in topContainer
   */
  function assertBodyInTop() {
    const fnName = 'assertBodyInTop';
    assertTrue(
        !!helpBubble.$.topBody,
        `${fnName} - body element should exist in topContainer`);
    assertTrue(
        !!helpBubble.$.mainBody,
        `${fnName} - body element should exist in main`);

    assertTrue(
        isVisible(helpBubble.$.topBody),
        `${fnName} - body element should be visible in topContainer`);
    assertFalse(
        isVisible(helpBubble.$.mainBody),
        `${fnName} - body element should not be visible in main`);

    assertFalse(
        helpBubble.$.topBody.hidden,
        `${fnName} - body element should not be hidden in topContainer`);
    // notice: we hide the entire main section here, not just the mainBody
    assertTrue(
        helpBubble.$.main.hidden, `${fnName} - main element should be hidden`);
  }

  /**
   * Asserts that body element is only visible in main
   */
  function assertBodyInMain() {
    const fnName = 'assertBodyInMain';
    assertTrue(
        !!helpBubble.$.topBody,
        `${fnName} - body element should exist in topContainer`);
    assertTrue(
        !!helpBubble.$.mainBody,
        `${fnName} - body element should exist in main`);

    assertFalse(
        isVisible(helpBubble.$.topBody),
        `${fnName} - body element should not be visible in topContainer`);
    assertTrue(
        isVisible(helpBubble.$.mainBody),
        `${fnName} - body element should be visible in main`);

    assertTrue(
        helpBubble.$.topBody.hidden,
        `${fnName} - body element should be hidden in topContainer`);
    // notice: we show the entire main section here, not just the mainBody
    assertFalse(
        helpBubble.$.main.hidden,
        `${fnName} - main element should not be hidden`);
  }

  /**
   * Create a promise that resolves after a given amount of time
   */
  async function sleep(milliseconds: number) {
    return new Promise((res) => {
      setTimeout(res, milliseconds);
    });
  }

  /**
   * Returns the current timestamp in milliseconds since UNIX epoch
   */
  function now() {
    return +new Date();
  }

  /**
   * Try/catch a function for some time, retrying after failures
   *
   * If the callback function succeeds, return early with the total time
   * If the callback always fails, throw the error after the last run
   */
  async function waitForSuccess(params: WaitForSuccessParams):
      Promise<number|null> {
    const startMs = now();
    let lastAttemptMs = startMs;
    let lastError: Error|null = null;
    let attempts = 0;
    while (now() - startMs < params.totalMs) {
      await sleep(params.retryIntervalMs);
      lastAttemptMs = now();
      try {
        params.assertionFn();
        return lastAttemptMs - startMs;
      } catch (e) {
        lastError = e as Error;
      }
      attempts++;
    }
    if (lastError !== null) {
      lastError.message = `[Attempts: ${attempts}, Total time: ${
          lastAttemptMs - startMs}ms]: ${lastError.message}`;
      throw lastError;
    }
    return Infinity;
  }

  setup(() => {
    document.body.innerHTML = getTrustedHTML`
    <div id='container'>
      <h1 id='title'>This is the title</h1>
      <p id='p1'>Some paragraph text</p>
      <ul id='bulletList'>
        <li>List item 1</li>
        <li>List item 2</li>
      </ul>
      <button id='short-button'>.</button>
      <br/>
      <button id='long-button'>
        This is the text inside a very long bubble ensuring edge alignment
      </button>
    </div>`;

    helpBubble = document.createElement('help-bubble');
    helpBubble.id = 'helpBubble';
    document.querySelector<HTMLElement>('#container')!.insertBefore(
        helpBubble, document.querySelector<HTMLElement>('#bulletList'));
  });

  test('bubble starts closed wth no anchor', () => {
    assertEquals(
        null, helpBubble.getAnchorElement(),
        'help bubble should have no anchor');
  });

  const HELP_BUBBLE_BODY = 'help bubble body';
  const HELP_BUBBLE_TITLE = 'help bubble title';

  test('help bubble shows and anchors correctly', async () => {
    const el = document.getElementById('p1')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.show(el);
    await microtasksFinished();

    assertEquals(
        document.querySelector<HTMLElement>('#p1'),
        helpBubble.getAnchorElement(),
        'help bubble should have correct anchor element');
    assertBodyInTop();
    assertEquals(
        HELP_BUBBLE_BODY, helpBubble.$.topBody.textContent!.trim(),
        'body content show match');
    assertTrue(isVisible(helpBubble), 'help bubble should be visible');
  });

  test('help bubble titles shows', async () => {
    const el = document.getElementById('p1')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.titleText = HELP_BUBBLE_TITLE;
    helpBubble.show(el);
    await microtasksFinished();

    assertTrue(isVisible(helpBubble), 'help bubble should be visible');
    const titleElement = helpBubble.$.title;
    assertTrue(!!titleElement, 'title element should exist');
    assertFalse(titleElement.hidden, 'title element should not be hidden');
    assertEquals(
        HELP_BUBBLE_TITLE, titleElement.textContent!.trim(),
        'title content should match');
    assertTrue(isVisible(titleElement), 'title element should be visible');
  });

  test('help bubble titles hides when no title set', async () => {
    const el = document.getElementById('p1')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.show(el);
    await microtasksFinished();

    assertTrue(isVisible(helpBubble), 'help bubble should be visible');
    const titleElement = helpBubble.$.title;
    assertTrue(!!titleElement, 'title element should exist');
    assertTrue(titleElement.hidden, 'title element should be hidden');
  });

  test('help bubble body icon shows when set', async () => {
    const el = document.getElementById('p1')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.bodyIconName = 'icon_name';
    helpBubble.bodyIconAltText = '';
    helpBubble.show(el);
    await microtasksFinished();

    assertTrue(isVisible(helpBubble), 'help bubble should be visible');
    const bodyIcon = helpBubble.$.bodyIcon;
    assertTrue(!!bodyIcon, 'body icon element should exist');
    assertFalse(bodyIcon.hidden, 'body icon element should not be hidden');
    assertTrue(isVisible(bodyIcon), 'body icon element should be visible');
    assertEquals(
        bodyIcon.querySelector('cr-icon')!.icon,
        'iph:icon_name',
        'bodyIcon passes icon name to cr-icon with iph namespace');
  });

  test('help bubble body icon is hidden when null', () => {
    const el = document.getElementById('p1')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.bodyIconName = null;
    helpBubble.bodyIconAltText = '';
    helpBubble.show(el);

    assertTrue(isVisible(helpBubble), 'help bubble should be visible');
    const bodyIcon = helpBubble.$.bodyIcon;
    assertTrue(!!bodyIcon, 'body icon element should exist');
    assertTrue(bodyIcon.hidden, 'body icon element should be hidden');
    assertFalse(isVisible(bodyIcon), 'body icon element should not be visible');
  });

  test('help bubble closes', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.show(el);
    await microtasksFinished();

    assertEquals(
        document.querySelector<HTMLElement>('#title'),
        helpBubble.getAnchorElement(),
        'title element should be anchor element');

    helpBubble.hide();
    await microtasksFinished();
    assertEquals(
        null, helpBubble.getAnchorElement(),
        'help bubble should not have anchor');
    assertFalse(isVisible(helpBubble), 'help bubble should not be visible');
  });

  test('help bubble open close open', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.show(el);
    await microtasksFinished();
    helpBubble.hide();
    await microtasksFinished();
    helpBubble.show(el);
    await microtasksFinished();
    assertEquals(
        document.querySelector<HTMLElement>('#title'),
        helpBubble.getAnchorElement(),
        'title element should be anchor element');
    assertBodyInTop();
    assertEquals(
        HELP_BUBBLE_BODY, helpBubble.$.topBody.textContent!.trim(),
        'body content should match');
    assertTrue(isVisible(helpBubble), 'help bubble should be visible');
  });

  test('help bubble close button has correct alt text', async () => {
    const CLOSE_TEXT: string = 'Close button text.';
    const ICON_TEXT: string = 'Body icon text.';
    const el = document.getElementById('title')!;
    helpBubble.closeButtonAltText = CLOSE_TEXT;
    helpBubble.bodyIconAltText = ICON_TEXT;
    helpBubble.show(el);
    await microtasksFinished();

    assertEquals(
        CLOSE_TEXT, helpBubble.$.close.getAttribute('aria-label'),
        'close button should have aria-label content');
    assertEquals(
        1, (helpBubble.$.close as HTMLElement).tabIndex,
        'close button should have tab index 1');
    assertEquals(
        ICON_TEXT, helpBubble.$.bodyIcon.getAttribute('aria-label'),
        'body icon should have aria-label content');
  });

  test('help bubble click close button generates event', async () => {
    let clicked: number = 0;
    const nativeId = getMockId();
    const callback = (e: HelpBubbleDismissedEvent) => {
      assertEquals(
          nativeId, e.detail.nativeId, 'dismiss event anchorId should match');
      assertFalse(
          e.detail.fromActionButton,
          'dismiss event should not be from action button');
      ++clicked;
    };
    helpBubble.addEventListener(HELP_BUBBLE_DISMISSED_EVENT, callback);
    const el = document.getElementById('title')!;
    helpBubble.nativeId = nativeId;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.show(el);
    await microtasksFinished();
    const closeButton = helpBubble.$.close;
    assertEquals(0, clicked, 'close button should not be clicked');
    closeButton.click();
    assertEquals(1, clicked, 'close button should be clicked once');
  });

  test('help bubble with timeout does not immediately emit event', async () => {
    let timedOut: number = 0;
    const nativeId = getMockId();
    const callback = (e: HelpBubbleTimedOutEvent) => {
      assertEquals(
          nativeId, e.detail.nativeId, 'timeout event anchorId should match');
      ++timedOut;
    };
    helpBubble.addEventListener(HELP_BUBBLE_TIMED_OUT_EVENT, callback);
    const el = document.getElementById('title')!;
    helpBubble.nativeId = nativeId;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.timeoutMs = 10 * 1000;  // 10s
    helpBubble.show(el);
    await microtasksFinished();
    assertEquals(0, timedOut, 'timeout should not be triggered');
  });

  test('help bubble with timeout generates event', async () => {
    const timeoutMs: number = 100;
    let timedOut: number = 0;
    const nativeId = getMockId();
    const callback = (e: HelpBubbleTimedOutEvent) => {
      assertEquals(
          nativeId, e.detail.nativeId, 'timeout event anchorId should match');
      ++timedOut;
    };
    helpBubble.addEventListener(HELP_BUBBLE_TIMED_OUT_EVENT, callback);
    const el = document.getElementById('title')!;
    helpBubble.nativeId = nativeId;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.timeoutMs = timeoutMs;  // 100ms
    helpBubble.show(el);
    await waitForSuccess({
      retryIntervalMs: 50,
      totalMs: 1500,
      assertionFn: () => assertEquals(1, timedOut, 'timeout should emit event'),
    }) as number;
  });

  test('help bubble without timeout does not generate event', async () => {
    let timedOut: number = 0;
    const nativeId = getMockId();
    const callback = (e: HelpBubbleTimedOutEvent) => {
      assertEquals(
          nativeId, e.detail.nativeId, 'timeout event anchorId should match');
      ++timedOut;
    };
    helpBubble.addEventListener(HELP_BUBBLE_TIMED_OUT_EVENT, callback);
    const el = document.getElementById('title')!;
    helpBubble.nativeId = nativeId;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.show(el);
    assertEquals(0, timedOut, 'timeout should not be triggered');
    await sleep(100);  // 100ms
    assertEquals(0, timedOut, 'timeout is never triggered');
  });

  test('help bubble adds one button', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.buttons = [{text: 'button1', isDefault: false}];
    helpBubble.show(el);
    await microtasksFinished();
    assertEquals(1, getNumButtons(), 'there should be one button');
    const button = helpBubble.getButtonForTesting(0);
    assertTrue(!!button, 'button should exist');
    assertEquals(
        helpBubble.buttons[0]!.text, button.textContent,
        'help bubble button content should match');
    assertFalse(
        button.classList.contains('default-button'),
        'button should not have default class');
  });

  test('help bubble adds several buttons', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.buttons = [
      {text: 'button1', isDefault: false},
      {text: 'button2', isDefault: false},
      {text: 'button3', isDefault: false},
    ];
    helpBubble.show(el);
    await microtasksFinished();
    assertEquals(3, getNumButtons(), 'there should be three buttons');
    for (let i: number = 0; i < 3; ++i) {
      const button = helpBubble.getButtonForTesting(i);
      assertTrue(!!button, 'button should exist');
      assertEquals(
          helpBubble.buttons[i]!.text, button.textContent,
          'button content should match');
      assertFalse(
          button.classList.contains('default-button'),
          'button should not have default class');
    }
  });

  test('help bubble adds default button', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.buttons = [{text: 'button1', isDefault: true}];
    helpBubble.show(el);
    await microtasksFinished();
    const button = helpBubble.getButtonForTesting(0);
    assertTrue(!!button, 'button should exist');
    assertTrue(
        button.classList.contains('default-button'),
        'button should have default class');
  });

  const THREE_BUTTONS_MIDDLE_DEFAULT: HelpBubbleButtonParams[] = [
    {text: 'button1', isDefault: false},
    {text: 'button2', isDefault: true},
    {text: 'button3', isDefault: false},
  ];

  test('help bubble adds default button among several', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;
    helpBubble.show(el);
    await microtasksFinished();
    assertEquals(3, getNumButtons(), 'there should be three buttons');

    // Make sure all buttons were created as expected, including the default
    // button.
    let defaultButton: CrButtonElement|null = null;
    for (let i: number = 0; i < 3; ++i) {
      const button = helpBubble.getButtonForTesting(i);
      assertTrue(!!button, 'button should exist');
      assertEquals(
          helpBubble.buttons[i]!.text, button.textContent,
          'button content should match');
      const isDefault = helpBubble.buttons[i]!.isDefault;
      assertEquals(
          isDefault, button.classList.contains('default-button'),
          `button should ${isDefault ? '' : 'not '}have have default class`);
      if (isDefault) {
        defaultButton = button;
      }
    }

    // Verify that the default button is in the expected position.
    assertTrue(!!defaultButton, 'default button should exist');
    const expectedIndex = HelpBubbleElement.isDefaultButtonLeading() ? 0 : 2;
    assertEquals(
        defaultButton, helpBubble.$.buttons.children.item(expectedIndex),
        'default button should be at correct index');

    // Verify tab indices
    let highestTabIndex = 0;
    for (const button of helpBubble.$.buttons.querySelectorAll<HTMLElement>(
             '[id*="action-button"')) {
      if (button.classList.contains('default-button')) {
        assertEquals(
            button.tabIndex, 1, 'default button should have tab index 1');
      } else {
        assertTrue(
            button.tabIndex > 1,
            'buttons should have tab index greater than 1');
      }
      highestTabIndex = Math.max(highestTabIndex, button.tabIndex);
    }

    assertFalse(helpBubble.$.close.hidden, 'close button should not be hidden');
    assertTrue(isVisible(helpBubble.$.close), 'close button should be visible');
    assertTrue(
        (helpBubble.$.close as HTMLElement).tabIndex > highestTabIndex,
        'close button should have highest tab index');
  });

  test('help bubble click action button generates event', async () => {
    let clicked: boolean;
    let buttonIndex: number;
    const nativeId = getMockId();
    const callback = (e: HelpBubbleDismissedEvent) => {
      assertEquals(nativeId, e.detail.nativeId, 'Check anchor.');
      assertTrue(e.detail.fromActionButton, 'Check fromActionButton.');
      assertTrue(e.detail.buttonIndex !== undefined, 'Check buttonIndex.');
      clicked = true;
      buttonIndex = e.detail.buttonIndex;
    };
    helpBubble.addEventListener(HELP_BUBBLE_DISMISSED_EVENT, callback);
    const el = document.getElementById('title')!;
    helpBubble.nativeId = nativeId;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;

    for (let i: number = 0; i < 3; ++i) {
      clicked = false;
      buttonIndex = -1;
      helpBubble.show(el);
      await microtasksFinished();
      const button = helpBubble.getButtonForTesting(i);
      assertTrue(!!button, 'button should exist');
      button.click();
      assertTrue(clicked, 'button should be clicked');
      assertEquals(i, buttonIndex, 'button index should match');
      helpBubble.hide();
    }
  });

  test('help bubble with no progress doesn\'t show progress', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;

    helpBubble.show(el);
    await microtasksFinished();

    assertEquals(
        0, getProgressIndicators().length,
        'there should not be progress indicators');
    assertBodyInTop();
  });

  test(
      'help bubble with no progress and title doesn\'t show progress',
      async () => {
        const el = document.getElementById('title')!;
        helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
        helpBubble.bodyText = HELP_BUBBLE_BODY;
        helpBubble.titleText = HELP_BUBBLE_TITLE;
        helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;

        helpBubble.show(el);
        await microtasksFinished();

        assertEquals(
            0, getProgressIndicators().length,
            'there should not be progress indicators');
        const titleElement = helpBubble.$.title;
        assertTrue(!!titleElement, 'title element should exist');
        assertFalse(titleElement.hidden, 'title element should not be hidden');
        assertBodyInMain();
      });

  test('help bubble with progress shows progress', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.progress = {current: 1, total: 3};
    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;

    helpBubble.show(el);
    await microtasksFinished();

    const elements = getProgressIndicators();
    assertEquals(3, elements.length, 'there should be three elements');
    assertTrue(
        elements.item(0)!.classList.contains('current-progress'),
        'element 0 should have current-progress class');
    assertTrue(
        elements.item(1)!.classList.contains('total-progress'),
        'element 1 should have total-progress class');
    assertTrue(
        elements.item(2)!.classList.contains('total-progress'),
        'element 2 should have total-progress class');
    assertBodyInMain();

    assertEquals(
        '1', helpBubble.$.progress.ariaValueMin,
        'progress start aria should be 1');
    assertEquals(
        '1', helpBubble.$.progress.ariaValueNow,
        'current progress aria should be 1');
    assertEquals(
        '3', helpBubble.$.progress.ariaValueMax,
        'total progress aria should be 3');
  });

  test('help bubble with progress and title shows progress', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.titleText = HELP_BUBBLE_TITLE;
    helpBubble.progress = {current: 1, total: 2};
    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;

    helpBubble.show(el);
    await microtasksFinished();

    const elements = getProgressIndicators();
    assertEquals(2, elements.length, 'there should be two elements');
    assertTrue(
        elements.item(0)!.classList.contains('current-progress'),
        'element 0 should have current-progress class');
    assertTrue(
        elements.item(1)!.classList.contains('total-progress'),
        'element 1 should have total-progress class');

    const titleElement = helpBubble.$.title;
    assertTrue(!!titleElement, 'title element should exist');
    assertTrue(titleElement.hidden, 'title element should be hidden');
    assertBodyInMain();
  });

  test('help bubble with full progress', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.progress = {current: 2, total: 2};

    helpBubble.show(el);
    await microtasksFinished();

    const elements = getProgressIndicators();
    assertEquals(2, elements.length, 'there should be two elements');
    assertTrue(
        elements.item(0)!.classList.contains('current-progress'),
        'element 0 should have current-progress class');
    assertTrue(
        elements.item(1)!.classList.contains('current-progress'),
        'element 1 should have current-progress class');
  });

  test('help bubble with empty progress', async () => {
    const el = document.getElementById('title')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_CENTER;
    helpBubble.bodyText = HELP_BUBBLE_BODY;
    helpBubble.progress = {current: 0, total: 2};

    helpBubble.show(el);
    await microtasksFinished();

    const elements = getProgressIndicators();
    assertEquals(2, elements.length, 'there should be two elements');
    assertTrue(
        elements.item(0)!.classList.contains('total-progress'),
        'element 0 should have total-progress class');
    assertTrue(
        elements.item(1)!.classList.contains('total-progress'),
        'element 1 should have total-progress class');
  });

  test('help bubble does not left-align with small anchor', async () => {
    const el = document.getElementById('short-button')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_LEFT;
    helpBubble.bodyText = HELP_BUBBLE_BODY;

    helpBubble.show(el);
    await microtasksFinished();

    const anchorRect = helpBubble.getAnchorElement()!.getBoundingClientRect();
    const helpBubbleRect = helpBubble.getBoundingClientRect();

    // Sanity checks
    assertEquals(
        document.querySelector<HTMLElement>('#short-button'),
        helpBubble.getAnchorElement(),
        'help bubble should have correct anchor element');
    assertBodyInTop();
    assertEquals(
        HELP_BUBBLE_BODY, helpBubble.$.topBody.textContent!.trim(),
        'body content show match');
    assertTrue(isVisible(helpBubble), 'help bubble should be visible');

    // Check that bubble exceeds left alignment with anchor so arrow
    // positions correctly
    assertTrue(
        helpBubbleRect.left < anchorRect.left,
        'bubble should position past the anchor\'s left edge');
  });

  test('help bubble left-aligns with large anchor', async () => {
    const el = document.getElementById('long-button')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_LEFT;
    helpBubble.bodyText = HELP_BUBBLE_BODY;

    helpBubble.show(el);
    await microtasksFinished();

    const anchorRect = helpBubble.getAnchorElement()!.getBoundingClientRect();
    const helpBubbleRect = helpBubble.getBoundingClientRect();

    // Sanity checks
    assertEquals(
        document.querySelector<HTMLElement>('#long-button'),
        helpBubble.getAnchorElement(),
        'help bubble should have correct anchor element');
    assertBodyInTop();
    assertEquals(
        HELP_BUBBLE_BODY, helpBubble.$.topBody.textContent!.trim(),
        'body content show match');
    assertTrue(isVisible(helpBubble), 'help bubble should be visible');

    // Check that bubble is left-aligned with anchor
    assertEquals(
        helpBubbleRect.left, anchorRect.left,
        'bubble and anchor should left-align');
  });

  test('help bubble does not right-align with small anchor', async () => {
    const el = document.getElementById('short-button')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_RIGHT;
    helpBubble.bodyText = HELP_BUBBLE_BODY;

    helpBubble.show(el);
    await microtasksFinished();

    const anchorRect = helpBubble.getAnchorElement()!.getBoundingClientRect();
    const helpBubbleRect = helpBubble.getBoundingClientRect();

    // Sanity checks
    assertEquals(
        document.querySelector<HTMLElement>('#short-button'),
        helpBubble.getAnchorElement(),
        'help bubble should have correct anchor element');
    assertBodyInTop();
    assertEquals(
        HELP_BUBBLE_BODY, helpBubble.$.topBody.textContent!.trim(),
        'body content show match');
    assertTrue(isVisible(helpBubble), 'help bubble should be visible');

    // Check that bubble exceeds right alignment with anchor so arrow
    // positions correctly
    assertTrue(
        helpBubbleRect.right > anchorRect.right,
        'bubble should position past the anchor\'s right edge');
  });

  test('help bubble right-aligns with large anchor', async () => {
    const el = document.getElementById('long-button')!;
    helpBubble.position = HelpBubbleArrowPosition.TOP_RIGHT;
    helpBubble.bodyText = HELP_BUBBLE_BODY;

    helpBubble.show(el);
    await microtasksFinished();

    const anchorRect = helpBubble.getAnchorElement()!.getBoundingClientRect();
    const helpBubbleRect = helpBubble.getBoundingClientRect();

    // Sanity checks
    assertEquals(
        document.querySelector<HTMLElement>('#long-button'),
        helpBubble.getAnchorElement(),
        'help bubble should have correct anchor element');
    assertBodyInTop();
    assertEquals(
        HELP_BUBBLE_BODY, helpBubble.$.topBody.textContent!.trim(),
        'body content show match');
    assertTrue(isVisible(helpBubble), 'help bubble should be visible');

    // Check that bubble is right-aligned with anchor
    assertEquals(
        helpBubbleRect.right, anchorRect.right,
        'bubble and anchor should right-align');
  });
});