chromium/chrome/test/data/webui/chromeos/emoji_picker/emoji_picker_test.ts

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

import {EMOJI_PICKER_TOTAL_EMOJI_WIDTH, EMOJI_VARIANTS_SHOWN, EmojiButton, EmojiSearch} from 'chrome://emoji-picker/emoji_picker.js';
import {assert} from 'chrome://resources/js/assert.js';
import {assertEquals, assertFalse, assertGT, assertLT, assertTrue} from 'chrome://webui-test/chai_assert.js';

import {assertCloseTo, dispatchMouseEvent, initialiseEmojiPickerForTest, isGroupButtonActive, timeout, waitForCondition, waitForEvent, waitWithTimeout} from './emoji_picker_test_util.js';


suite('<emoji-picker>', () => {
  const {emojiPicker, findInEmojiPicker, findEmojiFirstButton, readyPromise} =
      initialiseEmojiPickerForTest();

  setup(async () => {
    await readyPromise;
  });

  test('custom element should be defined', () => {
    assert(customElements.get('emoji-picker'));
  });

  test('first non-chevron, tab should be active by default', async () => {
    const button = findInEmojiPicker(
        'emoji-group-button[data-group="emoji-history"]', 'cr-icon-button');
    assertFalse(isGroupButtonActive(button));
  });

  test('second non-chevron tab should be inactive by default', () => {
    const button = findInEmojiPicker(
        'emoji-group-button[data-group="0"]', 'cr-icon-button');
    assertTrue(isGroupButtonActive(button));
  });

  test('Highlight bar should under emotions on start', () => {
    const button = findInEmojiPicker('#bar');
    assertCloseTo(
        EMOJI_PICKER_TOTAL_EMOJI_WIDTH, parseFloat(button!.style.left));
  });

  test('clicking second tab should activate it and scroll', async () => {
    // store initial scroll position of emoji groups.
    const emojiGroups = findInEmojiPicker('#groups')!;
    const initialScroll = emojiGroups.scrollTop;

    // History group doesn't exist when there is no history, so scrolling to
    // the first non-history group (0) may not trigger a scroll, so scroll to
    // group (1).
    const firstButton = findInEmojiPicker(
        'emoji-group-button[data-group="emoji-history"]', 'cr-icon-button');
    const thirdButton = findInEmojiPicker(
        'emoji-group-button[data-group="1"]', 'cr-icon-button');

    await waitForCondition(
        () => findEmojiFirstButton('[data-group="2"] > emoji-group'),
        'wait for groups to render so they can be scrolled to');
    thirdButton!.click();

    await waitForCondition(
        () => isGroupButtonActive(thirdButton) &&
            !isGroupButtonActive(firstButton),
        'wait for scroll to happen and buttons to update');

    const newScroll = emojiGroups.scrollTop;
    assertGT(newScroll, initialScroll);

    // check the highlight bar also moved
    const bar = findInEmojiPicker('#bar');
    assertGT(parseInt(bar!.style.left, 10), 0);
  });

  test('recently used should be hidden when empty', () => {
    const recentlyUsed =
        findInEmojiPicker('[data-group="emoji-history"] > emoji-group');
    assert(recentlyUsed!.classList.contains('hidden'));
  });

  test(
      'recently used should be populated after emoji is clicked normally',
      async () => {
        emojiPicker.updateIncognitoState(false);
        const emojiButton = await waitForCondition(
            () => findEmojiFirstButton('[data-group="0"] > emoji-group'),
            'wait for emoji-group to render');
        emojiButton!.click();

        const recentlyUsed = await waitForCondition(
            () => findEmojiFirstButton(
                '[data-group="emoji-history"] > emoji-group'),
            'wait for recently used to exists');

        // check text is correct.
        const recentText = recentlyUsed!.innerText;
        assertTrue(recentText.includes(String.fromCodePoint(128512)));
      });

  test(
      'clicking an emoji with no text field should copy it to the clipboard',
      async () => {
        emojiPicker.updateIncognitoState(false);
        const emojiButton = await waitForCondition(
            () => findEmojiFirstButton('[data-group="0"] > emoji-group'),
            'wait for emoji group to render');
        emojiButton!.click();

        await waitForCondition(
            () => findEmojiFirstButton(
                '[data-group="emoji-history"] > emoji-group'),
            'wait until recently used exists');

        await (waitForCondition(async () => {
          const clipboardtext = await navigator.clipboard.readText();
          return clipboardtext === String.fromCodePoint(128512);
        }, 'ensure correct text exists'));
      });

  test('recently-used should have variants for variant emoji', async () => {
    emojiPicker.updateIncognitoState(false);
    const emojiButton = (await waitForCondition(
        () => findInEmojiPicker(
            '[data-group="0"] > emoji-group', 'button[data-index="2"]'),
        'wait for emoji group to render'));
    emojiButton!.click();

    // wait until emoji exists in recently used section.
    const recentlyUsed = (await waitForCondition(
        () =>
            findEmojiFirstButton('[data-group="emoji-history"] > emoji-group'),
        'wait for recently used to render'));

    // check variants class is applied
    assertTrue(recentlyUsed!.classList.contains('has-variants'));
  });

  test(
      'recently-used should have no variants for non-variant emoji',
      async () => {
        emojiPicker.updateIncognitoState(false);
        const emojiButton = (await waitForCondition(
            () => findInEmojiPicker(
                '[data-group="0"] > emoji-group', 'button[data-index="0"]'),
            'ensure all emoji rendered'));
        emojiButton!.click();

        const recentlyUsed = (await waitForCondition(
            () => findEmojiFirstButton(
                '[data-group="emoji-history"] > emoji-group'),
            'wait for recently used to contain correct emoji'));

        // check variants class is not applied
        assertFalse(recentlyUsed!.classList.contains('has-variants'));
      });

  test(
      'recently-used should be empty after emoji is clicked in incognito mode',
      async () => {
        emojiPicker.updateIncognitoState(true);
        const emojiButton = await waitForCondition(
            () => findEmojiFirstButton('[data-group="0"] > emoji-group'),
            'ensure emoji picker fully rendered');
        emojiButton!.click();

        // Wait to ensure recents has a chance to render if we have a bug.
        await timeout(1000);

        const recentlyUsed =
            findInEmojiPicker('[data-group="emoji-history"] > emoji-group');
        assert(recentlyUsed!.classList.contains('hidden'));
      });

  test('recently used should be empty after clearing', async () => {
    emojiPicker.updateIncognitoState(false);
    const emojiButton = (await waitForCondition(
        () => findInEmojiPicker(
            '[data-group="0"] > emoji-group', 'button[data-index="1"]'),
        'ensure all emoji rendered'));
    emojiButton!.click();

    (await waitForCondition(
        () =>
            findEmojiFirstButton('[data-group="emoji-history"] > emoji-group'),
        'wait until emoji in recently used'));

    // click show clear button
    findInEmojiPicker('.group', '#show-clear')!.click();
    await waitForCondition(
        () => findInEmojiPicker('.group', '#clear-recents'),
        'wait for clear recents to appear');

    // click clear button
    findInEmojiPicker('.group', '#clear-recents')!.click();

    // Expect no more history.
    await waitForCondition(
        () => findInEmojiPicker('[data-group="emoji-history"] > emoji-group')!
                  .classList.contains('hidden'),
        'history failed to disappear');
  });


  suite('<emoji-variants>', () => {
    let firstEmojiButton: EmojiButton;

    const findEmojiVariants = (el: EmojiButton) => {
      const variants = (el.querySelector('emoji-variants'));
      return variants && variants.style.display !== 'none' ? variants : null;
    };

    setup(async () => {
      firstEmojiButton = (await waitForCondition(
                             () => findInEmojiPicker(
                                 '[data-group="0"] > emoji-group',
                                 '.emoji-button-container:nth-child(3)'),
                             'ensure all emoji are rendered')) as EmojiButton;

      // right click and wait for variants to appear.
      const variantsPromise = waitForEvent(emojiPicker, EMOJI_VARIANTS_SHOWN);
      dispatchMouseEvent(firstEmojiButton.querySelector('button')!, 2);

      await waitWithTimeout(
          variantsPromise, 1000, 'did not receive emoji variants event.');
      await waitForCondition(
          () => findEmojiVariants(firstEmojiButton),
          'emoji-variants failed to appear.', 5000);
    });

    test('right clicking emoji again should close popup', async () => {
      // right click again and variants should disappear.
      dispatchMouseEvent(firstEmojiButton.querySelector('button')!, 2);
      await waitForCondition(
          () => !findEmojiVariants(firstEmojiButton),
          'emoji-variants failed to disappear.');
    });

    test('clicking elsewhere should close popup', async () => {
      // click in some empty space of main emoji picker.
      emojiPicker.click();

      await waitForCondition(
          () => !findEmojiVariants(firstEmojiButton),
          'emoji-variants failed to disappear.');
    });

    test('opening different variants should close first variants', async () => {
      const emojiButton2 = await waitForCondition(
                               () => findInEmojiPicker(
                                   '[data-group="0"] > emoji-group',
                                   '.emoji-button-container:nth-child(4)'),
                               'ensure all emoji are rendered') as EmojiButton;

      // right click on second emoji button
      dispatchMouseEvent(emojiButton2.querySelector('button')!, 2);
      // ensure first popup disappears and second popup appears.
      await waitForCondition(
          () => !findEmojiVariants(firstEmojiButton),
          'first emoji-variants failed to disappear.');
      await waitForCondition(
          () => findEmojiVariants(emojiButton2),
          'second emoji-variants failed to appear.');
    });

    test('opening variants on the left side should not overflow', async () => {
      const groupsRect = emojiPicker.getBoundingClientRect();

      const variants = findEmojiVariants(firstEmojiButton);
      const variantsRect = variants!.getBoundingClientRect();

      assertLT(groupsRect.left, variantsRect.left);
      assertLT(variantsRect.right, groupsRect.right);
    });

    test('opening large variants (twice) should not overflow', async () => {
      const coupleEmojiButton =
          (await waitForCondition(
              () => findInEmojiPicker(
                  '[data-group="0"] > emoji-group',
                  '.emoji-button-container:nth-child(5)'),
              'ensure all emoji are rendered')) as EmojiButton;

      // listen for emoji variants event.
      const variantsPromise = waitForEvent(emojiPicker, EMOJI_VARIANTS_SHOWN);

      // right click on couple emoji to show 5x5 grid with skin tone.
      dispatchMouseEvent(coupleEmojiButton.querySelector('button')!, 2);
      const variants = await waitForCondition(
          () => findEmojiVariants(coupleEmojiButton),
          'Ensure correct emoji is rendered');

      // ensure variants are positioned before we get bounding rectangle.
      await waitWithTimeout(
          variantsPromise, 1000, 'did not receive emoji variants event.');
      const variantsRect = variants!.getBoundingClientRect();
      const pickerRect = emojiPicker.getBoundingClientRect();

      assertLT(pickerRect.left, variantsRect.left);
      assertLT(variantsRect.right, pickerRect.right);

      // Now close the variants and reopen, should still be positioned properly.
      emojiPicker.click();
      await waitForCondition(
          () => !findEmojiVariants(firstEmojiButton),
          'emoji-variants failed to disappear.');

      const variantsPromise2 = waitForEvent(emojiPicker, EMOJI_VARIANTS_SHOWN);
      // reshow
      dispatchMouseEvent(coupleEmojiButton.querySelector('button')!, 2);
      const variants2 = await waitForCondition(
          () => findEmojiVariants(coupleEmojiButton),
          'ensure that correct emoji is rendered')!;
      // ensure variants are positioned before we get bounding rectangle.
      await waitWithTimeout(
          variantsPromise2, 1000,
          'did not receive second emoji variants event.');
      const variantsRect2 = variants2!.getBoundingClientRect();

      assertLT(pickerRect.left, variantsRect2.left);
      assertLT(variantsRect2.right, pickerRect.right);
    });
  });

  suite('<emoji-search>', () => {
    test('works when there are no results', () => {
      // This test just ensures that no errors are thrown.
      const enterEvent = new KeyboardEvent(
          'keydown', {cancelable: true, key: 'Enter', keyCode: 13});
      const search = findInEmojiPicker('emoji-search') as EmojiSearch;
      search.onSearchKeyDown(enterEvent);
    });
    test('finds results in the second group', async () => {
      const search = findInEmojiPicker('emoji-search') as EmojiSearch;
      // This particular emoji only appears in the third tab of the test
      // ordering
      search.setSearchQuery('face with tears of joy');

      await waitForCondition(
          () => search.getNumSearchResults() > 0,
          'wait for search results to exist');
    });
    test('finds no results for garbage search', async () => {
      const search = findInEmojiPicker('emoji-search') as EmojiSearch;
      search!.setSearchQuery('THIS string should not match anything');

      await waitForCondition(
          () => findInEmojiPicker('emoji-search', '.no-result'),
          'wait for no results');
      assertEquals(search!.getNumSearchResults(), 0);
    });
  });
});