chromium/chrome/test/data/webui/app_home/app_list_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://apps/app_list.js';
import 'chrome://apps/app_item.js';
import 'chrome://apps/deprecated_apps_link.js';

import type {AppInfo, PageRemote} from 'chrome://apps/app_home.mojom-webui.js';
import {RunOnOsLoginMode} from 'chrome://apps/app_home.mojom-webui.js';
import type {AppHomeEmptyPageElement} from 'chrome://apps/app_home_empty_page.js';
import {AppHomeUserAction} from 'chrome://apps/app_home_utils.js';
import type {AppListElement} from 'chrome://apps/app_list.js';
import {BrowserProxy} from 'chrome://apps/browser_proxy.js';
import type {DeprecatedAppsLinkElement} from 'chrome://apps/deprecated_apps_link.js';
import type {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import {assertEquals, assertFalse, assertNull, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {microtasksFinished} from 'chrome://webui-test/test_util.js';

import {TestAppHomeBrowserProxy} from './test_app_home_browser_proxy.js';

interface AppList {
  appList: AppInfo[];
}

/**
 * A mock to intercept User Action logging calls and verify how many times they
 * were called.
 */
class MetricsPrivateMock {
  userActionMap: Map<string, number> = new Map();

  getUserActionCount(metricName: string): number {
    return this.userActionMap.get(metricName) || 0;
  }

  recordUserAction(metricName: string) {
    this.userActionMap.set(metricName, this.getUserActionCount(metricName) + 1);
  }
}

suite('AppListTest', () => {
  let appListElement: AppListElement;
  let apps: AppList;
  let testBrowserProxy: TestAppHomeBrowserProxy;
  let callbackRouterRemote: PageRemote;
  let testAppInfo: AppInfo;
  let deprecatedAppInfo: AppInfo;
  let metricsPrivateMock: MetricsPrivateMock;

  setup(async () => {
    apps = {
      appList: [
        {
          id: 'ahfgeienlihckogmohjhadlkjgocpleb',
          startUrl: {url: 'https://test.google.com/testapp1'},
          name: 'Test App 1',
          iconUrl: {
            url: 'chrome://app-icon/ahfgeienlihckogmohjhadlkjgocpleb/128/1',
          },
          mayShowRunOnOsLoginMode: true,
          mayToggleRunOnOsLoginMode: true,
          runOnOsLoginMode: RunOnOsLoginMode.kNotRun,
          isLocallyInstalled: true,
          mayUninstall: true,
          openInWindow: false,
          isDeprecatedApp: false,
          storePageUrl: null,
        },
        {
          id: 'ahfgeienlihckogmotestdlkjgocpleb',
          startUrl: {url: 'https://test.google.com/testapp2'},
          name: 'Test App 2',
          iconUrl: {
            url: 'chrome://app-icon/ahfgeienlihckogmotestdlkjgocpleb/128/1',
          },
          mayShowRunOnOsLoginMode: false,
          mayToggleRunOnOsLoginMode: false,
          runOnOsLoginMode: RunOnOsLoginMode.kNotRun,
          isLocallyInstalled: false,
          mayUninstall: false,
          openInWindow: false,
          isDeprecatedApp: false,
          storePageUrl: null,
        },
      ],
    };

    testAppInfo = {
      id: 'mmfbcljfglbokpmkimbfghdkjmjhdgbg',
      startUrl: {url: 'https://test.google.com/testapp3'},
      name: 'A Test App 3',
      iconUrl: {
        url: 'chrome://app-icon/mmfbcljfglbokpmkimbfghdkjmjhdgbg/128/1',
      },
      mayShowRunOnOsLoginMode: false,
      mayToggleRunOnOsLoginMode: false,
      runOnOsLoginMode: RunOnOsLoginMode.kNotRun,
      isLocallyInstalled: true,
      openInWindow: false,
      mayUninstall: true,
      isDeprecatedApp: false,
      storePageUrl: null,
    };
    deprecatedAppInfo = {
      id: 'mplpmdejoamenolpcojgegminhcnmibo',
      startUrl: {url: 'https://test.google.com/deprecated_app'},
      name: 'Deprecated App',
      iconUrl: {
        url: 'chrome://extension-icon/mplpmdejoamenolpcojgegminhcnmibo/128/1',
      },
      mayShowRunOnOsLoginMode: false,
      mayToggleRunOnOsLoginMode: false,
      runOnOsLoginMode: RunOnOsLoginMode.kNotRun,
      isLocallyInstalled: true,
      openInWindow: true,
      mayUninstall: true,
      isDeprecatedApp: true,
      storePageUrl: {
        url: '',
      },
    };
    metricsPrivateMock = new MetricsPrivateMock();
    chrome.metricsPrivate =
        metricsPrivateMock as unknown as typeof chrome.metricsPrivate;
    testBrowserProxy = new TestAppHomeBrowserProxy(apps);
    callbackRouterRemote = testBrowserProxy.callbackRouterRemote;
    BrowserProxy.setInstance(testBrowserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    appListElement = document.createElement('app-list');
    document.body.appendChild(appListElement);
    await microtasksFinished();
  });

  test('app list present', () => {
    assertTrue(!!appListElement);
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(AppHomeUserAction.APP_HOME_INIT));

    const appItems = appListElement.shadowRoot!.querySelectorAll('app-item');
    assertTrue(!!appItems);
    assertEquals(apps.appList.length, appItems.length);

    assertEquals(
        appItems[0]!.shadowRoot!.querySelector('#textContainer')!.textContent,
        apps.appList[0]!.name);
    assertEquals(
        appItems[0]!.shadowRoot!.querySelector<HTMLImageElement>(
                                    '#iconImage')!.src,
        apps.appList[0]!.iconUrl.url);

    assertEquals(
        appItems[1]!.shadowRoot!.querySelector('#textContainer')!.textContent,
        apps.appList[1]!.name);
    assertEquals(
        appItems[1]!.shadowRoot!.querySelector<HTMLImageElement>(
                                    '#iconImage')!.src,
        apps.appList[1]!.iconUrl.url + '?grayscale=true');
  });

  test('add/remove app', async () => {
    // Test adding an app.
    callbackRouterRemote.addApp(testAppInfo);
    await callbackRouterRemote.$.flushForTesting();
    let appItemList =
        Array.from(appListElement.shadowRoot!.querySelectorAll('app-item'));
    assertTrue(
        appItemList[0]!.shadowRoot!.querySelector(
                                       '#textContainer')!.textContent ===
        testAppInfo.name);

    // Test removing an app
    callbackRouterRemote.removeApp(testAppInfo);
    await callbackRouterRemote.$.flushForTesting();
    appItemList =
        Array.from(appListElement.shadowRoot!.querySelectorAll('app-item'));
    assertFalse(!!appItemList.find(
        appItem =>
            appItem.shadowRoot!.querySelector('#textContainer')!.textContent ===
            testAppInfo.name));
  });

  test('context menu locally installed', () => {
    // Get the first app item.
    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    assertFalse(contextMenu.open);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));
    assertTrue(contextMenu.open);
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.CONTEXT_MENU_TRIGGERED));

    assertTrue(apps.appList.length >= 1);
    const appInfo = apps.appList[0]!;

    const openInWindow =
        contextMenu.querySelector<CrCheckboxElement>('#openInWindow');
    assertTrue(!!openInWindow);
    assertEquals(openInWindow.hidden, !appInfo.isLocallyInstalled);
    assertEquals(openInWindow.checked, appInfo.openInWindow);

    const launchOnStartup =
        contextMenu.querySelector<CrCheckboxElement>('#launchOnStartup');
    assertTrue(!!launchOnStartup);
    assertEquals(launchOnStartup.hidden, !appInfo.mayShowRunOnOsLoginMode);

    assertEquals(
        launchOnStartup.checked,
        (appInfo.runOnOsLoginMode !== RunOnOsLoginMode.kNotRun));
    assertEquals(launchOnStartup.disabled, !appInfo.mayToggleRunOnOsLoginMode);

    assertTrue(!!contextMenu.querySelector('#createShortcut'));
    assertTrue(!!contextMenu.querySelector('#uninstall'));
    assertTrue(!!contextMenu.querySelector('#appSettings'));
    assertTrue(!!contextMenu.querySelector('#installLocally'));

    assertFalse(
        contextMenu.querySelector<HTMLElement>('#createShortcut')!.hidden);
    assertFalse(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);
    assertFalse(
        contextMenu.querySelector<HTMLButtonElement>('#uninstall')!.disabled);
    assertFalse(contextMenu.querySelector<HTMLElement>('#appSettings')!.hidden);
    assertTrue(
        contextMenu.querySelector<HTMLElement>('#installLocally')!.hidden);
  });

  test('context menu not locally installed', () => {
    // Get the second app item that's not locally installed.
    const appList = appListElement.shadowRoot!.querySelectorAll('app-item');
    assertEquals(appList.length, 2);
    const appItem = appList[1];
    assertTrue(!!appItem);

    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    assertFalse(contextMenu.open);
    assertEquals(
        0,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.CONTEXT_MENU_TRIGGERED));

    appItem.dispatchEvent(new CustomEvent('contextmenu'));
    assertTrue(contextMenu.open);

    assertTrue(contextMenu.querySelector<HTMLElement>('#openInWindow')!.hidden);
    assertTrue(
        contextMenu.querySelector<HTMLElement>('#launchOnStartup')!.hidden);
    assertTrue(
        contextMenu.querySelector<HTMLElement>('#createShortcut')!.hidden);
    assertTrue(contextMenu.querySelector<HTMLElement>('#appSettings')!.hidden);
    assertTrue(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);
    assertFalse(
        contextMenu.querySelector<HTMLElement>('#removeFromChrome')!.hidden);
    assertTrue(
        contextMenu.querySelector<HTMLButtonElement>(
                       '#removeFromChrome')!.disabled);
    assertFalse(
        contextMenu.querySelector<HTMLElement>('#installLocally')!.hidden);
  });

  test('toggle open in window', async () => {
    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    assertTrue(apps.appList.length >= 1);
    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    const openInWindow =
        contextMenu.querySelector<CrCheckboxElement>('#openInWindow');
    assertTrue(!!openInWindow);
    assertFalse(openInWindow.checked);
    assertFalse(apps.appList[0]!.openInWindow);

    // Clicking on the open in window context menu option to toggle
    // on or off.
    openInWindow.click();
    await callbackRouterRemote.$.flushForTesting();
    assertTrue(openInWindow.checked);
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.OPEN_IN_WINDOW_CHECKED));
    assertTrue(apps.appList[0]!.openInWindow);

    openInWindow.click();
    await callbackRouterRemote.$.flushForTesting();
    assertFalse(openInWindow.checked);
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.OPEN_IN_WINDOW_UNCHECKED));
    assertFalse(apps.appList[0]!.openInWindow);
  });

  test('toggle launch on startup', async () => {
    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    assertTrue(apps.appList.length >= 1);
    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    const launchOnStartup =
        contextMenu.querySelector<CrCheckboxElement>('#launchOnStartup');
    assertTrue(!!launchOnStartup);
    assertFalse(launchOnStartup.checked);
    assertEquals(apps.appList[0]!.runOnOsLoginMode, RunOnOsLoginMode.kNotRun);

    // Clicking on the launch on startup context menu option to toggle
    // on or off.
    launchOnStartup.click();
    await callbackRouterRemote.$.flushForTesting();
    assertTrue(launchOnStartup.checked);
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.LAUNCH_AT_STARTUP_CHECKED));
    assertEquals(apps.appList[0]!.runOnOsLoginMode, RunOnOsLoginMode.kWindowed);

    launchOnStartup.click();
    await callbackRouterRemote.$.flushForTesting();
    assertFalse(launchOnStartup.checked);
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.LAUNCH_AT_STARTUP_UNCHECKED));
    assertEquals(apps.appList[0]!.runOnOsLoginMode, RunOnOsLoginMode.kNotRun);
  });

  test('toggle launch on startup disabled', async () => {
    const appList = appListElement.shadowRoot!.querySelectorAll('app-item');
    assertEquals(appList.length, 2);
    const appItem = appList[1];
    assertTrue(!!appItem);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    const launchOnStartup =
        contextMenu.querySelector<CrCheckboxElement>('#launchOnStartup');
    assertTrue(!!launchOnStartup);
    assertFalse(launchOnStartup.checked);
    assertEquals(apps.appList[1]!.runOnOsLoginMode, RunOnOsLoginMode.kNotRun);

    // Clicking on the launch on startup context menu option should not toggle
    // if mayToggleRunOnOsLoginMode is false. The user actions should also
    // not get fired.
    launchOnStartup.click();
    await callbackRouterRemote.$.flushForTesting();
    assertFalse(launchOnStartup.checked);
    assertEquals(
        0,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.LAUNCH_AT_STARTUP_CHECKED));
    assertEquals(apps.appList[1]!.runOnOsLoginMode, RunOnOsLoginMode.kNotRun);
  });

  test('click uninstall', async () => {
    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    const uninstall =
        appItem.shadowRoot!.querySelector<HTMLElement>('#uninstall');
    assertTrue(!!uninstall);

    uninstall.click();
    await testBrowserProxy.fakeHandler.whenCalled('uninstallApp')
        .then((appId: string) => assertEquals(appId, apps.appList[0]!.id));
    assertEquals(
        1, metricsPrivateMock.getUserActionCount(AppHomeUserAction.UNINSTALL));
  });

  test('click app settings', async () => {
    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    const appSettings =
        appItem.shadowRoot!.querySelector<HTMLElement>('#appSettings');
    assertTrue(!!appSettings);

    appSettings.click();
    await testBrowserProxy.fakeHandler.whenCalled('showAppSettings')
        .then((appId: string) => assertEquals(appId, apps.appList[0]!.id));
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.OPEN_APP_SETTINGS));
  });

  test('click create shortcut', async () => {
    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    const createShortcut =
        appItem.shadowRoot!.querySelector<HTMLElement>('#createShortcut');
    assertTrue(!!createShortcut);

    createShortcut.click();
    await testBrowserProxy.fakeHandler.whenCalled('createAppShortcut')
        .then((appId: string) => assertEquals(appId, apps.appList[0]!.id));
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.CREATE_SHORTCUT));
  });

  test('click install locally', async () => {
    const appItem = appListElement.shadowRoot!.querySelectorAll('app-item')[1];
    assertTrue(!!appItem);

    assertEquals(
        appItem.shadowRoot!.querySelector<HTMLImageElement>('#iconImage')!.src,
        apps.appList[1]!.iconUrl.url + '?grayscale=true');

    assertEquals(appItem.ariaLabel, 'Test App 2 (not locally installed)');

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);

    assertTrue(contextMenu.querySelector<HTMLElement>('#openInWindow')!.hidden);
    assertTrue(
        contextMenu.querySelector<HTMLElement>('#createShortcut')!.hidden);
    assertTrue(contextMenu.querySelector<HTMLElement>('#appSettings')!.hidden);
    assertTrue(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);
    assertFalse(
        contextMenu.querySelector<HTMLElement>('#removeFromChrome')!.hidden);

    const installLocally =
        appItem.shadowRoot!.querySelector<HTMLElement>('#installLocally');
    assertTrue(!!installLocally);
    assertFalse(installLocally.hidden);

    installLocally.click();
    await testBrowserProxy.fakeHandler.whenCalled('installAppLocally')
        .then((appId: string) => assertEquals(appId, apps.appList[1]!.id));

    await callbackRouterRemote.$.flushForTesting();
    assertEquals(
        appItem.shadowRoot!.querySelector<HTMLImageElement>('#iconImage')!.src,
        apps.appList[1]!.iconUrl.url);

    assertEquals(appItem.ariaLabel, 'Test App 2');

    appItem.dispatchEvent(new CustomEvent('contextmenu'));

    assertFalse(
        contextMenu.querySelector<HTMLElement>('#openInWindow')!.hidden);
    assertFalse(
        contextMenu.querySelector<HTMLElement>('#createShortcut')!.hidden);
    assertFalse(contextMenu.querySelector<HTMLElement>('#appSettings')!.hidden);
    assertFalse(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);
    assertTrue(
        contextMenu.querySelector<HTMLElement>('#installLocally')!.hidden);
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.INSTALL_APP_LOCALLY));
  });

  test('click launch launches app', async () => {
    const appItem = appListElement.shadowRoot!.querySelectorAll('app-item')[1];
    assertTrue(!!appItem);

    const mouseEvent: MouseEvent = new MouseEvent('click', {
      button: 0,
      altKey: false,
      ctrlKey: false,
      metaKey: false,
      shiftKey: false,
    });

    appItem.dispatchEvent(mouseEvent);
    const [appId, clickEvent] =
        await testBrowserProxy.fakeHandler.whenCalled('launchApp');
    assertEquals(appId, apps.appList[1]!.id);
    assertEquals(clickEvent.button, mouseEvent.button);
    assertEquals(clickEvent.altKey, mouseEvent.altKey);
    assertEquals(clickEvent.ctrlKey, mouseEvent.ctrlKey);
    assertEquals(clickEvent.metaKey, mouseEvent.metaKey);
    assertEquals(clickEvent.shiftKey, mouseEvent.shiftKey);

    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.LAUNCH_WEB_APP));
  });

  test(
      'context menu right click opens corresponding menu for different app',
      () => {
        assertTrue(!!appListElement);

        const appItems =
            appListElement.shadowRoot!.querySelectorAll('app-item');
        assertTrue(!!appItems);
        assertEquals(apps.appList.length, appItems.length);
        assertTrue(!!appItems[0]);
        assertTrue(!!appItems[1]);

        appItems[0].dispatchEvent(new CustomEvent('contextmenu'));
        const contextMenu1 =
            appItems[0].shadowRoot!.querySelector('cr-action-menu');
        const contextMenu2 =
            appItems[1].shadowRoot!.querySelector('cr-action-menu');
        assertTrue(!!contextMenu1);
        assertTrue(!!contextMenu2);
        assertTrue(contextMenu1.open);
        assertFalse(contextMenu2.open);

        // Simulate right click on 2nd app such that the context menu for the
        // 2nd app shows up, and the context menu for the 1st app is hidden.
        appItems[1].dispatchEvent(new CustomEvent('contextmenu'));
        assertFalse(contextMenu1.open);
        assertTrue(contextMenu2.open);
      });

  test('context menu close on right click on document', () => {
    assertTrue(!!appListElement);

    const appItems = appListElement.shadowRoot!.querySelectorAll('app-item');
    assertTrue(!!appItems);
    assertEquals(apps.appList.length, appItems.length);
    assertTrue(!!appItems[0]);

    appItems[0].dispatchEvent(new CustomEvent('contextmenu'));
    const contextMenu = appItems[0].shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    assertTrue(contextMenu.open);

    // Simulate right click on document such that the context menu is hidden
    // again.
    document.dispatchEvent(new CustomEvent('contextmenu'));
    assertFalse(contextMenu.open);
  });

  test('navigate with arrow keys', async () => {
    appListElement.shadowRoot!.getElementById(
                                  'container')!.style.gridTemplateColumns =
        'repeat(2, max(100% / 2, 112px))';
    callbackRouterRemote.addApp(testAppInfo);
    await callbackRouterRemote.$.flushForTesting();
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'}));
    assertEquals(
        apps.appList[0]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'}));
    assertEquals(
        apps.appList[1]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'}));
    assertEquals(
        apps.appList[1]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
    assertEquals(
        apps.appList[0]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
    assertEquals(
        apps.appList[0]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'}));
    assertEquals(
        apps.appList[2]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'}));
    assertEquals(
        apps.appList[2]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'}));
    assertEquals(
        apps.appList[0]!.id, appListElement.shadowRoot!.activeElement?.id);
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'}));
    assertEquals(
        apps.appList[0]!.id, appListElement.shadowRoot!.activeElement?.id);
  });

  test('enter when focused on app launches app', async () => {
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'}));
    assertEquals(
        apps.appList[0]!.id, appListElement.shadowRoot!.activeElement?.id);

    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
    const [appId, clickEvent] =
        await testBrowserProxy.fakeHandler.whenCalled('launchApp');
    assertEquals(appId, apps.appList[0]!.id);
    assertEquals(clickEvent, null);
  });

  test('No deprecated apps means no deprecated app ux', async () => {
    const deprecatedAppsLink: DeprecatedAppsLinkElement =
        document.createElement('deprecated-apps-link');
    document.body.appendChild(deprecatedAppsLink);
    await microtasksFinished();

    assertTrue(!!deprecatedAppsLink);
    const linkContainer =
        deprecatedAppsLink.shadowRoot!.querySelector<HTMLImageElement>(
            '#container');
    assertNull(linkContainer, 'Deprecation link is not hidden.');

    const appItems = appListElement.shadowRoot!.querySelectorAll('app-item');
    assertTrue(!!appItems, 'No apps.');

    appItems.forEach((item) => {
      const deprecatedIcon =
          item!.shadowRoot!.querySelector<HTMLImageElement>('#deprecatedIcon')!;
      assertTrue(
          deprecatedIcon.hidden,
          'Non-deprecated app should not have deprecation icon');
    });
  });

  test('Deprecated link', async () => {
    testBrowserProxy.fakeHandler.addAppToList(deprecatedAppInfo);

    const deprecatedAppsLink: DeprecatedAppsLinkElement =
        document.createElement('deprecated-apps-link');
    document.body.appendChild(deprecatedAppsLink);
    await microtasksFinished();
    assertTrue(!!deprecatedAppsLink);
    const linkContainer =
        deprecatedAppsLink.shadowRoot!.querySelector<HTMLImageElement>(
            '#container');
    assertTrue(!!linkContainer);
  });

  test('Deprecated app icon', async () => {
    // Test adding an app.
    callbackRouterRemote.addApp(deprecatedAppInfo);
    await callbackRouterRemote.$.flushForTesting();

    const appItems = appListElement.shadowRoot!.querySelectorAll('.item');
    assertTrue(!!appItems, 'No apps.');

    let found = false;
    appItems.forEach((item) => {
      const deprecatedIcon =
          item!.shadowRoot!.querySelector<HTMLImageElement>('#deprecatedIcon')!;
      if (item!.id === deprecatedAppInfo.id) {
        found = true;
        assertFalse(
            deprecatedIcon.hidden,
            'Deprecated app should have deprecated icon visible');
      } else {
        assertTrue(
            deprecatedIcon.hidden,
            'Non-deprecated app should not have deprecation icon');
      }
    });
    assertTrue(found, 'Deprecated item not found.');
  });

  test('Clicking deprecated app', async () => {
    // Test adding an app.
    callbackRouterRemote.addApp(deprecatedAppInfo);
    await callbackRouterRemote.$.flushForTesting();

    const appItem =
        appListElement.shadowRoot!.querySelector('#' + deprecatedAppInfo.id)!;
    assertTrue(!!appItem, 'No apps.');

    const mouseEvent: MouseEvent = new MouseEvent('click', {
      button: 0,
      altKey: false,
      ctrlKey: false,
      metaKey: false,
      shiftKey: false,
    });
    appItem.dispatchEvent(mouseEvent);

    await testBrowserProxy.fakeHandler.whenCalled('launchApp');
    assertEquals(
        1,
        metricsPrivateMock.getUserActionCount(
            AppHomeUserAction.LAUNCH_DEPRECATED_APP));
  });

  test('Clicking deprecation link calls handler', async () => {
    // Test adding an app.
    callbackRouterRemote.addApp(deprecatedAppInfo);
    testBrowserProxy.fakeHandler.addAppToList(deprecatedAppInfo);
    await callbackRouterRemote.$.flushForTesting();

    const deprecatedAppsLink: DeprecatedAppsLinkElement =
        document.createElement('deprecated-apps-link');
    document.body.appendChild(deprecatedAppsLink);
    await microtasksFinished();

    assertTrue(!!deprecatedAppsLink);
    const link = deprecatedAppsLink.shadowRoot!.querySelector<HTMLImageElement>(
        '#deprecated-apps-link')!;

    link.click();

    await testBrowserProxy.fakeHandler.whenCalled('launchDeprecatedAppDialog');
  });

  test('Empty app page', async () => {
    const emptyPage: AppHomeEmptyPageElement =
        document.createElement('app-home-empty-page');
    document.body.appendChild(emptyPage);
    await microtasksFinished();

    callbackRouterRemote.removeApp(apps.appList[0]!);
    callbackRouterRemote.removeApp(apps.appList[1]!);
    callbackRouterRemote.removeApp(deprecatedAppInfo);
    await callbackRouterRemote.$.flushForTesting();

    const appItems = appListElement.shadowRoot!.querySelectorAll('app-item');
    assertEquals(appItems.length, 0);

    const text: HTMLParagraphElement =
        emptyPage.shadowRoot!.querySelector<HTMLParagraphElement>('p')!;
    assertEquals(text.innerText, 'Web apps that you install appear here');

    const button: HTMLAnchorElement =
        emptyPage.shadowRoot!.querySelector<HTMLAnchorElement>('a')!;
    assertEquals(
        button.href, 'https://support.google.com/chrome?p=install_web_apps');
    assertEquals(button.innerText, 'Learn how to install web apps');
  });

  test('context menu not closed on checkbox click', async () => {
    // Test for crbug.com/1435592: Clicking the checkbox options on
    // the context menu does not close it.
    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    appItem.dispatchEvent(new CustomEvent('contextmenu'));
    assertTrue(apps.appList.length >= 1);

    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    const launchOnStartup =
        contextMenu.querySelector<CrCheckboxElement>('#launchOnStartup');
    assertTrue(!!launchOnStartup);
    assertFalse(launchOnStartup.checked);
    const openInWindow =
        contextMenu.querySelector<CrCheckboxElement>('#openInWindow');
    assertTrue(!!openInWindow);
    assertFalse(openInWindow.checked);

    // Launch on Startup check.
    launchOnStartup.click();
    await callbackRouterRemote.$.flushForTesting();
    assertTrue(launchOnStartup.checked);
    assertFalse(contextMenu.hidden);

    // Open In Window check.
    openInWindow.click();
    await callbackRouterRemote.$.flushForTesting();
    assertTrue(openInWindow.checked);
    assertFalse(contextMenu.hidden);
  });

  test('context menu opens on shift+f10 triggered on focused app', async () => {
    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'}));
    assertEquals(
        apps.appList[0]!.id, appListElement.shadowRoot!.activeElement?.id);

    document.dispatchEvent(
        new KeyboardEvent('keydown', {key: 'F10', shiftKey: true}));

    const appItem = appListElement.shadowRoot!.querySelector('app-item');
    assertTrue(!!appItem);

    const contextMenu = appItem.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!contextMenu);
    assertFalse(contextMenu.hidden);
  });
});