chromium/ui/file_manager/file_manager/foreground/elements/files_tooltip_unittest.ts

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

import './files_tooltip.js';

import {getTrustedHTML} from 'chrome://resources/js/static_types.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';

import type {FilesTooltip} from './files_tooltip.js';

let chocolateButton: HTMLButtonElement;
let cherriesButton: HTMLButtonElement;
let cheeseButton: HTMLButtonElement;
let otherButton: HTMLButtonElement;
let tooltip: FilesTooltip;

const WINDOW_EDGE_PADDING = 6;

export function setUp() {
  document.body.innerHTML = getTrustedHTML`
  <style type="text/css">
   button {
     display: flex;
     height: 32px;
     margin: 30px;
     width: 32px;
   }

   #container {
     display: flex;
     justify-content: space-between;
   }

   files-tooltip {
     background: yellow;
     box-sizing: border-box;
     position: absolute;
     text-align: center;
     width: 100px;
   }
  </style>

  <!-- Targets for tooltip testing. -->
  <div id="container">
    <button id="chocolate" aria-label="Chocolate!"></button>
    <button id="cherries" aria-label="Cherries!"></button>
  </div>

  <button id="cheese" aria-label="Cheese!" show-card-tooltip></button>

  <!-- Button without a tooltip. -->
  <button id="other"></button>

  <!-- Polymer files tooltip element. -->
  <files-tooltip></files-tooltip>
`;
  chocolateButton = document.querySelector<HTMLButtonElement>('#chocolate')!;
  cherriesButton = document.querySelector<HTMLButtonElement>('#cherries')!;
  cheeseButton = document.querySelector<HTMLButtonElement>('#cheese')!;
  otherButton = document.querySelector<HTMLButtonElement>('#other')!;

  tooltip = document.querySelector<FilesTooltip>('files-tooltip')!;
  assertNotEquals('none', window.getComputedStyle(tooltip).display);
  assertEquals('0', window.getComputedStyle(tooltip).opacity);

  tooltip.addTargets([chocolateButton, cherriesButton, cheeseButton]);
}

function waitForMutation(target: FilesTooltip) {
  return new Promise<void>((fulfill) => {
    const observer = new MutationObserver(_ => {
      observer.disconnect();
      fulfill();
    });
    observer.observe(target, {attributes: true});
  });
}

export async function testFocus() {
  chocolateButton.focus();

  await waitForMutation(tooltip);
  const label1 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Chocolate!', label1.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));
  assertEquals('6px', tooltip.style.left);
  assertEquals('78px', tooltip.style.top);

  cherriesButton.focus();
  await waitForMutation(tooltip);

  const label2 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Cherries!', label2.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));

  const expectedLeft = document.body.offsetWidth - tooltip.offsetWidth -
      WINDOW_EDGE_PADDING + 'px';
  assertEquals(expectedLeft, tooltip.style.left);
  assertEquals('78px', tooltip.style.top);

  otherButton.focus();
  await waitForMutation(tooltip);
  assertFalse(!!tooltip.getAttribute('visible'));
}

export async function testFocusWithLink() {
  cherriesButton.dataset['tooltipLinkHref'] = 'https://cherries.com';
  cherriesButton.dataset['tooltipLinkAriaLabel'] =
      'Click here to get more cherries';
  cherriesButton.dataset['tooltipLinkText'] = 'More cherries';

  chocolateButton.focus();

  await waitForMutation(tooltip);

  const label1 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Chocolate!', label1.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));
  assertEquals('6px', tooltip.style.left);
  assertEquals('78px', tooltip.style.top);

  cherriesButton.focus();
  await waitForMutation(tooltip);
  // Check the label.
  const label2 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Cherries!', label2.textContent?.trim());
  // Check the link: it should be visible now.
  const link1 = tooltip.shadowRoot!.querySelector<HTMLLinkElement>('#link')!;
  assertEquals(link1.getAttribute('aria-hidden'), 'false');
  assertEquals('More cherries', link1.textContent?.trim());
  assertEquals(
      'Click here to get more cherries', link1.getAttribute('aria-label'));
  assertEquals('https://cherries.com', link1.getAttribute('href'));

  assertEquals(tooltip.getAttribute('aria-hidden'), 'false');
  assertTrue(tooltip.hasAttribute('visible'));

  const expectedLeft = document.body.offsetWidth - tooltip.offsetWidth -
      WINDOW_EDGE_PADDING + 'px';
  assertEquals(expectedLeft, tooltip.style.left);
  assertEquals('78px', tooltip.style.top);

  chocolateButton.focus();
  await waitForMutation(tooltip);
  // Check the label.
  const label3 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Chocolate!', label3.textContent?.trim());
  // Check the link: it should be hidden and cleared out.
  const link2 = tooltip.shadowRoot!.querySelector<HTMLLinkElement>('#link')!;
  assertEquals(link2.getAttribute('aria-hidden'), 'true');
  assertEquals('', link2.textContent?.trim());
  assertFalse(link2.hasAttribute('aria-label'));
  assertEquals('#', link2.getAttribute('href'));
}

export async function testFocusWithLabelChange() {
  chocolateButton.focus();

  await waitForMutation(tooltip);
  const label1 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Chocolate!', label1.textContent?.trim());
  // Change the button's aria-label attribute and the tooltip should
  // also update.
  chocolateButton.setAttribute('aria-label', 'New chocolate!');

  tooltip.updateTooltipText(chocolateButton);
  await waitForMutation(tooltip);

  const label2 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('New chocolate!', label2.textContent?.trim());
}

export async function testHover() {
  chocolateButton.dispatchEvent(new MouseEvent('mouseover'));

  await waitForMutation(tooltip);
  const label1 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Chocolate!', label1.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));
  assertEquals(tooltip.getAttribute('aria-hidden'), 'false');

  assertEquals('6px', tooltip.style.left);
  assertEquals('78px', tooltip.style.top);

  chocolateButton.dispatchEvent(new MouseEvent('mouseout'));
  cherriesButton.dispatchEvent(new MouseEvent('mouseover'));
  await waitForMutation(tooltip);

  const label2 = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Cherries!', label2.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));

  const expectedLeft = document.body.offsetWidth - tooltip.offsetWidth -
      WINDOW_EDGE_PADDING + 'px';
  assertEquals(expectedLeft, tooltip.style.left);
  assertEquals('78px', tooltip.style.top);

  cherriesButton.dispatchEvent(new MouseEvent('mouseout'));
  await waitForMutation(tooltip);

  assertFalse(!!tooltip.getAttribute('visible'));
}

export async function testClickHides() {
  chocolateButton.dispatchEvent(new MouseEvent('mouseover', {bubbles: true}));

  await waitForMutation(tooltip);

  const label = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Chocolate!', label.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));
  // Hiding here is synchronous. Dispatch the event asynchronously,
  // so the mutation observer is started before hiding.
  setTimeout(() => {
    document.body.dispatchEvent(new MouseEvent('mousedown'));
  });
  await waitForMutation(tooltip);

  assertFalse(tooltip.hasAttribute('visible'));
  assertEquals(tooltip.getAttribute('aria-hidden'), 'true');
}

export async function testCardTooltipHover() {
  cheeseButton.dispatchEvent(new MouseEvent('mouseover'));

  await waitForMutation(tooltip);

  const label = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Cheese!', label.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));
  assertEquals(tooltip.getAttribute('aria-hidden'), 'false');

  assertEquals('card-tooltip', tooltip.className);
  assertEquals('card-label', label.className);

  assertEquals('38px', tooltip.style.left);
  assertEquals('162px', tooltip.style.top);

  cheeseButton.dispatchEvent(new MouseEvent('mouseout'));
  await waitForMutation(tooltip);

  assertFalse(!!tooltip.getAttribute('visible'));
}

export async function testCardTooltipRTL() {
  document.documentElement.setAttribute('dir', 'rtl');
  document.body.setAttribute('dir', 'rtl');

  cheeseButton.dispatchEvent(new MouseEvent('mouseover'));

  await waitForMutation(tooltip);

  const label = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Cheese!', label.textContent?.trim());
  assertTrue(tooltip.hasAttribute('visible'));
  assertEquals(tooltip.getAttribute('aria-hidden'), 'false');

  assertEquals('card-tooltip', tooltip.className);
  assertEquals('card-label', label.className);

  // A border with 1px insets (top=bottom=left=right=1px) will be
  // applied to the window when drak/light feature is enabled. See
  // more details at crrev.com/c/3656414.
  assertTrue(`962px` === tooltip.style.left || `960px` === tooltip.style.left);
  assertEquals('162px', tooltip.style.top);

  cheeseButton.dispatchEvent(new MouseEvent('mouseout'));
  await waitForMutation(tooltip);

  // revert back document direction to not impact other tests.
  document.documentElement.setAttribute('dir', 'ltr');
  document.body.setAttribute('dir', 'ltr');
}

export async function testCardTooltipWithLinkHover() {
  cheeseButton.dataset['tooltipLinkHref'] = 'https://cheese.com';
  cheeseButton.dataset['tooltipLinkAriaLabel'] =
      'Click here to get more cheese';
  cheeseButton.dataset['tooltipLinkText'] = 'More cheese';
  cheeseButton.dispatchEvent(new MouseEvent('mouseover'));

  await waitForMutation(tooltip);

  // Check the label.
  const label = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Cheese!', label.textContent?.trim());
  // Check the link: it should be visible now.
  const link = tooltip.shadowRoot!.querySelector<HTMLLinkElement>('#link')!;
  assertEquals(link.getAttribute('aria-hidden'), 'false');
  assertEquals('More cheese', link.textContent?.trim());
  assertEquals(
      'Click here to get more cheese', link.getAttribute('aria-label'));
  assertEquals('https://cheese.com', link.getAttribute('href'));

  assertTrue(tooltip.hasAttribute('visible'));
  assertEquals(tooltip.getAttribute('aria-hidden'), 'false');

  assertTrue(tooltip.classList.contains('card-tooltip'));
  assertEquals('card-label', label.className);

  assertEquals('38px', tooltip.style.left);
  assertEquals('162px', tooltip.style.top);

  assertTrue(tooltip.classList.contains('link-tooltip'));

  cheeseButton.dispatchEvent(new MouseEvent('mouseout'));
  await waitForMutation(tooltip);

  assertFalse(!!tooltip.getAttribute('visible'));
}

export async function testTooltipWithIncompleteLinkHover() {
  cheeseButton.dataset['tooltipLinkHref'] = 'https://cheese.com';
  cheeseButton.dispatchEvent(new MouseEvent('mouseover'));

  await waitForMutation(tooltip);

  // Check the label.
  const label = tooltip.shadowRoot!.querySelector<HTMLDivElement>('#label')!;
  assertEquals('Cheese!', label.textContent?.trim());
  // Check the link: it should be hidden since not all required
  // attributes are set.
  const link = tooltip.shadowRoot!.querySelector<HTMLLinkElement>('#link')!;
  assertEquals(link.getAttribute('aria-hidden'), 'true');
  assertEquals('', link.textContent?.trim());
  assertFalse(link.hasAttribute('aria-label'));
  assertEquals('#', link.getAttribute('href'));

  assertTrue(tooltip.hasAttribute('visible'));
  assertEquals(tooltip.getAttribute('aria-hidden'), 'false');

  assertTrue(tooltip.classList.contains('card-tooltip'));
  assertEquals('card-label', label.className);

  assertEquals('38px', tooltip.style.left);
  assertEquals('162px', tooltip.style.top);

  assertFalse(tooltip.classList.contains('link-tooltip'));

  cheeseButton.dispatchEvent(new MouseEvent('mouseout'));
  await waitForMutation(tooltip);

  assertFalse(!!tooltip.getAttribute('visible'));
}