chromium/third_party/google-closure-library/closure/goog/ui/emoji/emojipicker_test.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.ui.emoji.EmojiPickerTest');
goog.setTestOnly();

const Component = goog.require('goog.ui.Component');
const Emoji = goog.require('goog.ui.emoji.Emoji');
const EmojiPalette = goog.requireType('goog.ui.emoji.EmojiPalette');
const EmojiPicker = goog.require('goog.ui.emoji.EmojiPicker');
const EventHandler = goog.require('goog.events.EventHandler');
const SpriteInfo = goog.require('goog.ui.emoji.SpriteInfo');
const TagName = goog.require('goog.dom.TagName');
const classlist = goog.require('goog.dom.classlist');
const events = goog.require('goog.testing.events');
const style = goog.require('goog.style');
const testSuite = goog.require('goog.testing.testSuite');

let handler;

// 26 emoji
const emojiGroup1 = [
  'Emoji 1',
  [
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
  ],
];

// 20 emoji
const emojiGroup2 = [
  'Emoji 2',
  [
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
  ],
];

// 20 emoji
const emojiGroup3 = [
  'Emoji 3',
  [
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
  ],
];

const sprite = '../../demos/emoji/sprite.png';
const sprite2 = '../../demos/emoji/sprite2.png';

/**
 * Creates a SpriteInfo object with the specified properties. If the image is
 * sprited via CSS, then only the first parameter needs a value. If the image
 * is sprited via metadata, then the first parameter should be left null.
 * @param {?string} cssClass CSS class to properly display the sprited image.
 * @param {string=} url Url of the sprite image.
 * @param {number=} width Width of the image being sprited.
 * @param {number=} height Height of the image being sprited.
 * @param {number=} xOffset Positive x offset of the image being sprited within
 *     the sprite.
 * @param {number=} yOffset Positive y offset of the image being sprited within
 *     the sprite.
 * @param {boolean=} animated Whether the sprite info is for an animated emoji.
 */
function si(
    cssClass, url = undefined, width = undefined, height = undefined,
    xOffset = undefined, yOffset = undefined, animated = undefined) {
  return new SpriteInfo(
      cssClass, url, width, height, xOffset, yOffset, animated);
}

// Contains a mix of sprited emoji via css, sprited emoji via metadata, and
// non-sprited emoji
const spritedEmoji1 = [
  'Emoji 1',
  [
    ['../../demos/emoji/200.gif', 'std.200', si('SPRITE_200')],
    ['../../demos/emoji/201.gif', 'std.201', si('SPRITE_201')],
    ['../../demos/emoji/202.gif', 'std.202', si('SPRITE_202')],
    ['../../demos/emoji/203.gif', 'std.203', si('SPRITE_203')],
    ['../../demos/emoji/204.gif', 'std.204', si('SPRITE_204')],
    ['../../demos/emoji/200.gif', 'std.200', si('SPRITE_200')],
    ['../../demos/emoji/201.gif', 'std.201', si('SPRITE_201')],
    ['../../demos/emoji/202.gif', 'std.202', si('SPRITE_202')],
    ['../../demos/emoji/203.gif', 'std.203', si('SPRITE_203')],
    ['../../demos/emoji/2BE.gif', 'std.2BE', si(null, sprite, 18, 18, 36, 54)],
    ['../../demos/emoji/2BF.gif', 'std.2BF', si(null, sprite, 18, 18, 0, 126)],
    ['../../demos/emoji/2C0.gif', 'std.2C0', si(null, sprite, 18, 18, 18, 305)],
    ['../../demos/emoji/2C1.gif', 'std.2C1', si(null, sprite, 18, 18, 0, 287)],
    ['../../demos/emoji/2C2.gif', 'std.2C2', si(null, sprite, 18, 18, 18, 126)],
    ['../../demos/emoji/2C3.gif', 'std.2C3', si(null, sprite, 18, 18, 36, 234)],
    ['../../demos/emoji/2C4.gif', 'std.2C4', si(null, sprite, 18, 18, 36, 72)],
    ['../../demos/emoji/2C5.gif', 'std.2C5', si(null, sprite, 18, 18, 54, 54)],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
    ['../../demos/emoji/204.gif', 'std.204'],
    ['../../demos/emoji/200.gif', 'std.200'],
    ['../../demos/emoji/201.gif', 'std.201'],
    ['../../demos/emoji/202.gif', 'std.202'],
    ['../../demos/emoji/203.gif', 'std.203'],
  ],
];

// This group contains a mix of sprited emoji via css, sprited emoji via
// metadata, and non-sprited emoji.
/** @suppress {checkTypes} suppression added to enable type checking */
const spritedEmoji2 = [
  'Emoji 1',
  [
    ['../../demos/emoji/200.gif', 'std.200', si('SPRITE_200')],
    ['../../demos/emoji/201.gif', 'std.201', si('SPRITE_201')],
    ['../../demos/emoji/202.gif', 'std.202', si('SPRITE_202')],
    ['../../demos/emoji/203.gif', 'std.203', si('SPRITE_203')],
    ['../../demos/emoji/204.gif', 'std.204', si('SPRITE_204')],
    ['../../demos/emoji/200.gif', 'std.200', si('SPRITE_200')],
    ['../../demos/emoji/201.gif', 'std.201', si('SPRITE_201')],
    ['../../demos/emoji/202.gif', 'std.202', si('SPRITE_202')],
    ['../../demos/emoji/203.gif', 'std.203', si('SPRITE_203')],
    ['../../demos/emoji/2BE.gif', 'std.2BE', si(null, sprite, 18, 18, 36, 54)],
    ['../../demos/emoji/2BF.gif', 'std.2BF', si(null, sprite, 18, 18, 0, 126)],
    ['../../demos/emoji/2C0.gif', 'std.2C0', si(null, sprite, 18, 18, 18, 305)],
    ['../../demos/emoji/2C1.gif', 'std.2C1', si(null, sprite, 18, 18, 0, 287)],
    ['../../demos/emoji/2C2.gif', 'std.2C2', si(null, sprite, 18, 18, 18, 126)],
    ['../../demos/emoji/2C3.gif', 'std.2C3', si(null, sprite, 18, 18, 36, 234)],
    ['../../demos/emoji/2C4.gif', 'std.2C4', si(null, sprite, 18, 18, 36, 72)],
    ['../../demos/emoji/2C5.gif', 'std.2C5', si(null, sprite, 18, 18, 54, 54)],
    ['../../demos/emoji/2C6.gif', 'std.2C6'],
    ['../../demos/emoji/2C7.gif', 'std.2C7'],
    ['../../demos/emoji/2C8.gif', 'std.2C8'],
    ['../../demos/emoji/2C9.gif', 'std.2C9'],
    [
      '../../demos/emoji/2CA.gif',
      'std.2CA',
      si(null, sprite2, 18, 20, 36, 72, 1),
    ],
    [
      '../../demos/emoji/2E3.gif',
      'std.2E3',
      si(null, sprite2, 18, 18, 0, 0, 1),
    ],
    [
      '../../demos/emoji/2EF.gif',
      'std.2EF',
      si(null, sprite2, 18, 20, 0, 300, 1),
    ],
    [
      '../../demos/emoji/2F1.gif',
      'std.2F1',
      si(null, sprite2, 18, 18, 0, 320, 1),
    ],
  ],
];

const emojiGroups = [emojiGroup1, emojiGroup2, emojiGroup3];

/**
 * Helper for testDelayedLoad. Returns true if the two paths end with the same
 * file.
 * E.g., ('../../cool.gif', 'file:///home/usr/somewhere/cool.gif') --> true
 * @param {string} path1 First url
 * @param {string} path2 Second url
 */
function checkPathsEndWithSameFile(path1, path2) {
  const pieces1 = path1.split('/');
  const file1 = pieces1[pieces1.length - 1];
  const pieces2 = path2.split('/');
  const file2 = pieces2[pieces2.length - 1];

  return file1 == file2;
}

/**
 * Gets the emoji URL from a palette element. Palette elements are divs or
 * imgs wrapped in an outer div. The returns the background-image if it's a div,
 * or the src attribute if it's an image.
 * @param {Element} element Element to get the image url for
 * @return {string}
 * @suppress {strictMissingProperties} suppression added to enable type checking
 */
function getImageUrl(element) {
  /** @suppress {checkTypes} suppression added to enable type checking */
  element = element.firstChild;  // get the wrapped element
  if (element.tagName == TagName.IMG) {
    return element.src;
  } else {
    /** @suppress {checkTypes} suppression added to enable type checking */
    let url = style.getStyle(element, 'background-image');
    url = url.replace(/url\(/, '');
    url = url.replace(/\)/, '');
    return url;
  }
}

/**
 * Checks that the content of an emojipicker page is all images pointing to
 * the default img.
 * @param {!EmojiPalette} page The page of the picker to check
 * @param {string} defaultImgUrl The url of the default img
 */
function checkContentIsDefaultImg(page, defaultImgUrl) {
  const content = page.getContent();

  for (let i = 0; i < content.length; i++) {
    const url = getImageUrl(content[i]);
    assertTrue(
        `img src should be ${defaultImgUrl} but is ${url}`,
        checkPathsEndWithSameFile(url, defaultImgUrl));
  }
}

/**
 * Checks that the content of an emojipicker page is the specified emoji and
 * the default img after the emoji are all used.
 * @param {!EmojiPalette} page The page of the picker to check
 * @param {Array<Array<string>>} emojiList List of emoji that should be in the
 *     palette
 * @param {string} defaultImgUrl The url of the default img
 * @suppress {checkTypes} suppression added to enable type checking
 */
function checkContentIsEmojiImages(page, emojiList, defaultImg) {
  const content = page.getContent();

  for (let i = 0; i < content.length; i++) {
    const url = getImageUrl(content[i]);
    if (i < emojiList.length) {
      assertTrue(
          `Paths should end with the same file: ${url}, ` + emojiList[i][0],
          checkPathsEndWithSameFile(url, emojiList[i][0]));
    } else {
      assertTrue(
          `Paths should end with the same file: ${url}, ${defaultImg}`,
          checkPathsEndWithSameFile(url, defaultImg));
    }
  }
}

/**
 * Checks and verifies the structure of a non-progressively-rendered
 * emojipicker.
 * @param {!EmojiPalette} palette Emoji palette to check.
 * @param {Array<Array<string>>} emoji Emoji that should be in the palette.
 * @suppress {strictMissingProperties} suppression added to enable type checking
 */
function checkStructureForNonProgressivePicker(palette, emoji) {
  // We can hackily check the items by selecting an item and then getting the
  // selected item.
  for (let i = 0; i < emoji[1].length; i++) {
    palette.setSelectedIndex(i);
    const emojiInfo = emoji[1][i];
    const cell = palette.getSelectedItem();
    const inner = /** @type {Element} */ (cell.firstChild);

    // Check that the cell is a div wrapped around something else, and that the
    // outer div contains the goomoji attribute
    assertEquals(
        'The palette item should be a div wrapped around something',
        cell.tagName, 'DIV');
    assertNotNull('The outer div is not wrapped around another element', inner);
    assertEquals(
        'The palette item should have the goomoji attribute',
        cell.getAttribute(Emoji.ATTRIBUTE), emojiInfo[1]);
    assertEquals(
        'The palette item should have the data-goomoji attribute',
        cell.getAttribute(Emoji.DATA_ATTRIBUTE), emojiInfo[1]);

    // Now check the contents of the cells
    const spriteInfo = emojiInfo[2];
    if (spriteInfo) {
      assertEquals(inner.tagName, 'DIV');
      const cssClass = spriteInfo.getCssClass();
      if (cssClass) {
        assertTrue(
            'Sprite should have its CSS class set',
            classlist.contains(inner, cssClass));
      } else {
        checkPathsEndWithSameFile(
            style.getStyle(inner, 'background-image'), spriteInfo.getUrl());
        assertEquals(
            spriteInfo.getWidthCssValue(), style.getStyle(inner, 'width'));
        assertEquals(
            spriteInfo.getHeightCssValue(), style.getStyle(inner, 'height'));
        assertEquals(
            (spriteInfo.getXOffsetCssValue() + ' ' +
             spriteInfo.getYOffsetCssValue())
                .replace(/px/g, '')
                .replace(/pt/g, ''),
            style.getStyle(inner, 'background-position')
                .replace(/px/g, '')
                .replace(/pt/g, ''));
      }
    } else {
      // A non-sprited emoji is just an img
      assertEquals(inner.tagName, 'IMG');
      checkPathsEndWithSameFile(inner.src, emojiInfo[0]);
    }
  }
}

/**
 * Checks and verifies the structure of a progressively-rendered emojipicker.
 * @param {!EmojiPalette} palette Emoji palette to check.
 * @param {Array<Array<string>>} emoji Emoji that should be in the palette.
 * @suppress {strictMissingProperties,checkTypes} suppression added to enable
 * type checking
 */
function checkStructureForProgressivePicker(palette, emoji) {
  // We can hackily check the items by selecting an item and then getting the
  // selected item.
  for (let i = 0; i < emoji[1].length; i++) {
    palette.setSelectedIndex(i);
    const emojiInfo = emoji[1][i];
    const cell = palette.getSelectedItem();
    const inner = /** @type {Element} */ (cell.firstChild);

    // Check that the cell is a div wrapped around something else, and that the
    // outer div contains the goomoji attribute
    assertEquals(
        'The palette item should be a div wrapped around something',
        cell.tagName, 'DIV');
    assertNotNull('The outer div is not wrapped around another element', inner);
    assertEquals(
        'The palette item should have the goomoji attribute',
        cell.getAttribute(Emoji.ATTRIBUTE), emojiInfo[1]);
    assertEquals(
        'The palette item should have the data-goomoji attribute',
        cell.getAttribute(Emoji.DATA_ATTRIBUTE), emojiInfo[1]);

    // Now check the contents of the cells
    const spriteInfo = emojiInfo[2];
    if (spriteInfo) {
      const cssClass = spriteInfo.getCssClass();
      if (cssClass) {
        assertEquals('DIV', inner.tagName);
        assertTrue(
            'Sprite should have its CSS class set',
            classlist.contains(inner, cssClass));
      } else {
        // There's an inner div wrapping an img tag
        assertEquals('DIV', inner.tagName);
        const img = inner.firstChild;
        assertNotNull('Div should be wrapping something', img);
        assertEquals('IMG', img.tagName);
        checkPathsEndWithSameFile(img.src, spriteInfo.getUrl());
        assertEquals(
            spriteInfo.getWidthCssValue(), style.getStyle(inner, 'width'));
        assertEquals(
            spriteInfo.getHeightCssValue(), style.getStyle(inner, 'height'));
        assertEquals(
            spriteInfo.getXOffsetCssValue().replace(/px/, '').replace(/pt/, ''),
            style.getStyle(img, 'left').replace(/px/, '').replace(/pt/, ''));
        assertEquals(
            spriteInfo.getYOffsetCssValue().replace(/px/, '').replace(/pt/, ''),
            style.getStyle(img, 'top').replace(/px/, '').replace(/pt/, ''));
      }
    } else {
      // A non-sprited emoji is just an img
      assertEquals(inner.tagName, 'IMG');
      checkPathsEndWithSameFile(inner.src, emojiInfo[0]);
    }
  }
}

/**
 * Checks and verifies the structure of a non-progressive fast-loading picker
 * after the animated emoji have loaded.
 * @param {!EmojiPalette} palette Emoji palette to check.
 * @param {Array<Array<string>>} emoji Emoji that should be in the palette.
 * @suppress {strictMissingProperties} suppression added to enable type checking
 */
function checkPostLoadStructureForFastLoadNonProgressivePicker(palette, emoji) {
  for (let i = 0; i < emoji[1].length; i++) {
    palette.setSelectedIndex(i);
    const emojiInfo = emoji[1][i];
    const cell = palette.getSelectedItem();
    const inner = /** @type {Element} */ (cell.firstChild);

    // Check that the cell is a div wrapped around something else, and that the
    // outer div contains the goomoji attribute
    assertEquals(
        'The palette item should be a div wrapped around something',
        cell.tagName, 'DIV');
    assertNotNull('The outer div is not wrapped around another element', inner);
    assertEquals(
        'The palette item should have the goomoji attribute',
        cell.getAttribute(Emoji.ATTRIBUTE), emojiInfo[1]);
    assertEquals(
        'The palette item should have the data-goomoji attribute',
        cell.getAttribute(Emoji.DATA_ATTRIBUTE), emojiInfo[1]);

    // Now check the contents of the cells
    const url = emojiInfo[0];  // url of the animated emoji
    const spriteInfo = emojiInfo[2];
    if (spriteInfo) {
      assertEquals(inner.tagName, 'DIV');
      if (spriteInfo.isAnimated()) {
        const img = new Image();
        img.src = url;
        checkPathsEndWithSameFile(
            style.getStyle(inner, 'background-image'), url);
        assertEquals(
            String(img.width),
            style.getStyle(inner, 'width')
                .replace(/px/g, '')
                .replace(/pt/g, ''));
        assertEquals(
            String(img.height),
            style.getStyle(inner, 'height')
                .replace(/px/g, '')
                .replace(/pt/g, ''));
        assertEquals(
            '0 0',
            style.getStyle(inner, 'background-position')
                .replace(/px/g, '')
                .replace(/pt/g, ''));
      } else {
        const cssClass = spriteInfo.getCssClass();
        if (cssClass) {
          assertTrue(
              'Sprite should have its CSS class set',
              classlist.contains(inner, cssClass));
        } else {
          checkPathsEndWithSameFile(
              style.getStyle(inner, 'background-image'), spriteInfo.getUrl());
          assertEquals(
              spriteInfo.getWidthCssValue(), style.getStyle(inner, 'width'));
          assertEquals(
              spriteInfo.getHeightCssValue(), style.getStyle(inner, 'height'));
          assertEquals(
              (spriteInfo.getXOffsetCssValue() + ' ' +
               spriteInfo.getYOffsetCssValue())
                  .replace(/px/g, '')
                  .replace(/pt/g, ''),
              style.getStyle(inner, 'background-position')
                  .replace(/px/g, '')
                  .replace(/pt/g, ''));
        }
      }
    } else {
      // A non-sprited emoji is just an img
      assertEquals(inner.tagName, 'IMG');
      checkPathsEndWithSameFile(inner.src, emojiInfo[0]);
    }
  }
}

/**
 * Checks and verifies the structure of a progressive fast-loading picker
 * after the animated emoji have loaded.
 * @param {!EmojiPalette} palette Emoji palette to check.
 * @param {Array<Array<string>>} emoji Emoji that should be in the palette.
 * @suppress {strictMissingProperties,checkTypes} suppression added to enable
 * type checking
 */
function checkPostLoadStructureForFastLoadProgressivePicker(palette, emoji) {
  for (let i = 0; i < emoji[1].length; i++) {
    palette.setSelectedIndex(i);
    const emojiInfo = emoji[1][i];
    const cell = palette.getSelectedItem();
    const inner = /** @type {Element} */ (cell.firstChild);

    // Check that the cell is a div wrapped around something else, and that the
    // outer div contains the goomoji attribute
    assertEquals(
        'The palette item should be a div wrapped around something',
        cell.tagName, 'DIV');
    assertNotNull('The outer div is not wrapped around another element', inner);
    assertEquals(
        'The palette item should have the goomoji attribute',
        cell.getAttribute(Emoji.ATTRIBUTE), emojiInfo[1]);
    assertEquals(
        'The palette item should have the data-goomoji attribute',
        cell.getAttribute(Emoji.DATA_ATTRIBUTE), emojiInfo[1]);

    // Now check the contents of the cells
    const url = emojiInfo[0];  // url of the animated emoji
    const spriteInfo = emojiInfo[2];
    if (spriteInfo) {
      if (spriteInfo.isAnimated()) {
        const testImg = new Image();
        testImg.src = url;
        const img = inner.firstChild;
        checkPathsEndWithSameFile(img.src, url);
        assertEquals(testImg.width, img.width);
        assertEquals(testImg.height, img.height);
        assertEquals(
            '0',
            style.getStyle(img, 'left').replace(/px/g, '').replace(/pt/g, ''));
        assertEquals(
            '0',
            style.getStyle(img, 'top').replace(/px/g, '').replace(/pt/g, ''));
      } else {
        const cssClass = spriteInfo.getCssClass();
        if (cssClass) {
          assertEquals('DIV', inner.tagName);
          assertTrue(
              'Sprite should have its CSS class set',
              classlist.contains(inner, cssClass));
        } else {
          // There's an inner div wrapping an img tag
          assertEquals('DIV', inner.tagName);
          const img = inner.firstChild;
          assertNotNull('Div should be wrapping something', img);
          assertEquals('IMG', img.tagName);
          checkPathsEndWithSameFile(img.src, spriteInfo.getUrl());
          assertEquals(
              spriteInfo.getWidthCssValue(), style.getStyle(inner, 'width'));
          assertEquals(
              spriteInfo.getHeightCssValue(), style.getStyle(inner, 'height'));
          assertEquals(
              spriteInfo.getXOffsetCssValue().replace(/px/, '').replace(
                  /pt/, ''),
              style.getStyle(img, 'left').replace(/px/, '').replace(/pt/, ''));
          assertEquals(
              spriteInfo.getYOffsetCssValue().replace(/px/, '').replace(
                  /pt/, ''),
              style.getStyle(img, 'top').replace(/px/, '').replace(/pt/, ''));
        }
      }
    } else {
      // A non-sprited emoji is just an img
      assertEquals(inner.tagName, 'IMG');
      checkPathsEndWithSameFile(inner.src, emojiInfo[0]);
    }
  }
}

testSuite({
  setUp() {
    handler = new EventHandler();
  },

  tearDown() {
    handler.removeAll();
  },

  testConstructAndRenderOnePageEmojiPicker() {
    const picker = new EmojiPicker('../../demos/emoji/none.gif');
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.render();
    picker.dispose();
  },

  testConstructAndRenderMultiPageEmojiPicker() {
    const picker = new EmojiPicker('../../demos/emoji/none.gif');
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.addEmojiGroup(emojiGroup2[0], emojiGroup2[1]);
    picker.addEmojiGroup(emojiGroup3[0], emojiGroup3[1]);
    picker.render();
    picker.dispose();
  },

  testExitDocumentCleansUpProperlyForSinglePageEmojiPicker() {
    const picker = new EmojiPicker('../../demos/emoji/none.gif');
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.render();
    picker.enterDocument();
    picker.exitDocument();
    picker.dispose();
  },

  testExitDocumentCleansUpProperlyForMultiPageEmojiPicker() {
    const picker = new EmojiPicker('../../demos/emoji/none.gif');
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.addEmojiGroup(emojiGroup2[0], emojiGroup2[1]);
    picker.render();
    picker.enterDocument();
    picker.exitDocument();
    picker.dispose();
  },

  testNumGroups() {
    const picker = new EmojiPicker('../../demos/emoji/none.gif');

    for (let i = 0; i < emojiGroups.length; i++) {
      picker.addEmojiGroup(emojiGroups[i][0], emojiGroups[i][1]);
    }

    assertTrue(picker.getNumEmojiGroups() == emojiGroups.length);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testAdjustNumRowsIfNecessaryIsCorrect() {
    const picker = new EmojiPicker('../../demos/emoji/none.gif');
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.setAutoSizeByColumnCount(true);
    picker.setNumColumns(5);
    assertEquals(5, picker.getNumColumns());
    assertEquals(EmojiPicker.DEFAULT_NUM_ROWS, picker.getNumRows());

    picker.adjustNumRowsIfNecessary_();

    // The emojiGroup has 26 emoji. ceil(26/5) should give 6 rows.
    assertEquals(6, picker.getNumRows());

    // Change col count to 10, should give 3 rows.
    picker.setNumColumns(10);
    picker.adjustNumRowsIfNecessary_();
    assertEquals(3, picker.getNumRows());

    // Add another gruop, with 20 emoji. Deliberately set the number of rows too
    // low. It should adjust it to three to accommodate the emoji in the first
    // group.
    picker.addEmojiGroup(emojiGroup2[0], emojiGroup2[1]);
    picker.setNumColumns(10);
    picker.setNumRows(2);
    picker.adjustNumRowsIfNecessary_();
    assertEquals(3, picker.getNumRows());
  },

  testNonDelayedLoadPaletteCreationForSinglePagePicker() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.render();

    const page = picker.getPage(0);
    assertTrue(
        'Page should be in the document but is not', page.isInDocument());

    // The content should be the actual emoji images now, with the remainder set
    // to the default img
    checkContentIsEmojiImages(page, emojiGroup1[1], defaultImg);

    picker.dispose();
  },

  testNonDelayedLoadPaletteCreationForMultiPagePicker() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);

    for (let i = 0; i < emojiGroups.length; i++) {
      picker.addEmojiGroup(emojiGroups[i][0], emojiGroups[i][1]);
    }

    picker.render();

    for (let i = 0; i < emojiGroups.length; i++) {
      const page = picker.getPage(i);
      assertTrue(
          `Page ${i} should be in the document but is not`,
          page.isInDocument());
      checkContentIsEmojiImages(page, emojiGroups[i][1], defaultImg);
    }

    picker.dispose();
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testDelayedLoadPaletteCreationForSinglePagePicker() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(true);
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.render();

    // At this point the picker should have pages filled with the default img
    checkContentIsDefaultImg(picker.getPage(0), defaultImg);

    // Now load the images
    picker.loadImages();

    const page = picker.getPage(0);
    assertTrue(
        'Page should be in the document but is not', page.isInDocument());

    // The content should be the actual emoji images now, with the remainder set
    // to the default img
    checkContentIsEmojiImages(page, emojiGroup1[1], defaultImg);

    picker.dispose();
  },

  testGetSelectedEmoji() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.render();

    const palette = picker.getPage(0);

    // No emoji should be selected yet
    assertUndefined(palette.getSelectedEmoji());

    // Artificially select the first emoji
    palette.setSelectedIndex(0);

    // Now we should get the first emoji back. See emojiGroup1 above.
    const emoji = palette.getSelectedEmoji();
    assertEquals(emoji.getId(), 'std.200');
    assertEquals(emoji.getUrl(), '../../demos/emoji/200.gif');

    picker.dispose();
  },

  testGetSelectedEmoji_click() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.render();

    const palette = picker.getPage(0);
    // Artificially select the an emoji
    palette.setSelectedIndex(2);
    const element = palette.getSelectedItem();
    palette.setSelectedIndex(0);  // Select a different emoji.

    let eventSent;
    handler.listen(picker, Component.EventType.ACTION, (e) => {
      eventSent = e;
    });
    events.fireClickSequence(element, undefined, undefined, {shiftKey: false});

    // Now we should get the first emoji back. See emojiGroup1 above.
    const emoji = picker.getSelectedEmoji();
    assertEquals(emoji.getId(), 'std.202');
    assertEquals(emoji.getUrl(), '../../demos/emoji/202.gif');
    assertFalse(eventSent.shiftKey);

    picker.dispose();
  },

  testGetSelectedEmoji_shiftClick() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.render();

    const palette = picker.getPage(0);
    // Artificially select the an emoji
    palette.setSelectedIndex(3);
    const element = palette.getSelectedItem();
    palette.setSelectedIndex(0);  // Select a different emoji.

    let eventSent;
    handler.listen(picker, Component.EventType.ACTION, (e) => {
      eventSent = e;
    });
    events.fireClickSequence(element, undefined, undefined, {shiftKey: true});

    // Now we should get the first emoji back. See emojiGroup1 above.
    const emoji = picker.getSelectedEmoji();
    assertEquals(emoji.getId(), 'std.203');
    assertEquals(emoji.getUrl(), '../../demos/emoji/203.gif');
    assertTrue(eventSent.shiftKey);

    picker.dispose();
  },

  testGetSelectedEmoji_urlPrefix() {
    const defaultImg = 'a/b/../../../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.addEmojiGroup(emojiGroup1[0], emojiGroup1[1]);
    picker.setUrlPrefix('a/b/../../');
    picker.render();

    const palette = picker.getPage(0);
    // Artificially select the an emoji
    palette.setSelectedIndex(0);
    palette.dispatchEvent(Component.EventType.ACTION);

    // Now we should get the first emoji back. See emojiGroup1 above.
    const emoji = picker.getSelectedEmoji();
    assertEquals('std.200', emoji.getId());
    assertEquals('a/b/../../../../demos/emoji/200.gif', emoji.getUrl());

    picker.dispose();
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testPreLoadCellConstructionForFastLoadingNonProgressive() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.setManualLoadOfAnimatedEmoji(true);
    picker.setProgressiveRender(false);
    picker.addEmojiGroup(spritedEmoji2[0], spritedEmoji2[1]);
    picker.render();

    const palette = picker.getPage(0);

    checkStructureForNonProgressivePicker(palette, spritedEmoji2);

    picker.dispose();
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testPreLoadCellConstructionForFastLoadingProgressive() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.setManualLoadOfAnimatedEmoji(true);
    picker.setProgressiveRender(true);
    picker.addEmojiGroup(spritedEmoji2[0], spritedEmoji2[1]);
    picker.render();

    const palette = picker.getPage(0);

    checkStructureForProgressivePicker(palette, spritedEmoji2);

    picker.dispose();
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testCellConstructionForNonProgressiveRenderingSpriting() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.addEmojiGroup(spritedEmoji1[0], spritedEmoji1[1]);
    picker.render();

    const palette = picker.getPage(0);

    checkStructureForNonProgressivePicker(palette, spritedEmoji1);
    picker.dispose();
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testCellConstructionForProgressiveRenderingSpriting() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(false);
    picker.setProgressiveRender(true);
    picker.addEmojiGroup(spritedEmoji1[0], spritedEmoji1[1]);
    picker.render();

    const palette = picker.getPage(0);

    checkStructureForProgressivePicker(palette, spritedEmoji1);

    picker.dispose();
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testDelayedLoadPaletteCreationForMultiPagePicker() {
    const defaultImg = '../../demos/emoji/none.gif';
    const picker = new EmojiPicker(defaultImg);
    picker.setDelayedLoad(true);

    for (let i = 0; i < emojiGroups.length; i++) {
      picker.addEmojiGroup(emojiGroups[i][0], emojiGroups[i][1]);
    }

    picker.render();

    // At this point the picker should have pages filled with the default img
    for (let i = 0; i < emojiGroups.length; i++) {
      checkContentIsDefaultImg(picker.getPage(i), defaultImg);
    }

    // Now load the images
    picker.loadImages();

    // The first page should be loaded
    let page = picker.getPage(0);
    assertTrue(
        'Page should be in the document but is not', page.isInDocument());
    checkContentIsEmojiImages(page, emojiGroups[0][1], defaultImg);

    // The other pages should all be filled with the default img since they are
    // lazily loaded
    for (let i = 1; i < 3; i++) {
      checkContentIsDefaultImg(picker.getPage(i), defaultImg);
    }

    // Activate the other two pages so that their images get loaded, and check
    // that they're now loaded correctly
    const tabPane = picker.getTabPane();

    for (let i = 1; i < 3; i++) {
      tabPane.setSelectedIndex(i);
      page = picker.getPage(i);
      assertTrue(page.isInDocument());
      checkContentIsEmojiImages(page, emojiGroups[i][1], defaultImg);
    }

    picker.dispose();
  },
});