chromium/chrome/test/data/webui/new_tab_page/app_test.ts

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

import type {Module} from 'chrome://new-tab-page/lazy_load.js';
import {counterfactualLoad, ModuleDescriptor, ModuleRegistry} from 'chrome://new-tab-page/lazy_load.js';
import type {AppElement} from 'chrome://new-tab-page/new_tab_page.js';
import {$$, BackgroundManager, BrowserCommandProxy, CUSTOMIZE_CHROME_BUTTON_ELEMENT_ID, CustomizeDialogPage, NewTabPageProxy, NtpCustomizeChromeEntryPoint, NtpElement, VoiceAction, WindowProxy} from 'chrome://new-tab-page/new_tab_page.js';
import type {PageRemote} from 'chrome://new-tab-page/new_tab_page.mojom-webui.js';
import {CustomizeChromeSection, NtpBackgroundImageSource, PageCallbackRouter, PageHandlerRemote} from 'chrome://new-tab-page/new_tab_page.mojom-webui.js';
import type {CrToastElement} from 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
import {Command, CommandHandlerRemote} from 'chrome://resources/js/browser_command.mojom-webui.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {isMac} from 'chrome://resources/js/platform.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import {assertDeepEquals, assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import type {MetricsTracker} from 'chrome://webui-test/metrics_test_support.js';
import {fakeMetricsPrivate} from 'chrome://webui-test/metrics_test_support.js';
import type {TestMock} from 'chrome://webui-test/test_mock.js';
import {eventToPromise, microtasksFinished} from 'chrome://webui-test/test_util.js';

import {assertNotStyle, assertStyle, createBackgroundImage, createTheme, installMock} from './test_support.js';

suite('NewTabPageAppTest', () => {
  let app: AppElement;
  let windowProxy: TestMock<WindowProxy>;
  let handler: TestMock<PageHandlerRemote>;
  let callbackRouterRemote: PageRemote;
  let metrics: MetricsTracker;
  let moduleRegistry: TestMock<ModuleRegistry>;
  let backgroundManager: TestMock<BackgroundManager>;
  let moduleResolver: PromiseResolver<Module[]>;

  const url: URL = new URL(location.href);
  const backgroundImageLoadTime: number = 123;

  setup(async () => {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;

    windowProxy = installMock(WindowProxy);
    handler = installMock(
        PageHandlerRemote,
        mock => NewTabPageProxy.setInstance(mock, new PageCallbackRouter()));
    handler.setResultFor('getMostVisitedSettings', Promise.resolve({
      customLinksEnabled: false,
      shortcutsVisible: false,
    }));
    handler.setResultFor('getBackgroundCollections', Promise.resolve({
      collections: [],
    }));
    handler.setResultFor('getDoodle', Promise.resolve({
      doodle: null,
    }));
    handler.setResultFor('getModulesIdNames', Promise.resolve({data: []}));
    windowProxy.setResultMapperFor('matchMedia', () => ({
                                                   addListener() {},
                                                   addEventListener() {},
                                                   removeListener() {},
                                                   removeEventListener() {},
                                                 }));
    windowProxy.setResultFor('waitForLazyRender', Promise.resolve());
    windowProxy.setResultFor('createIframeSrc', '');
    windowProxy.setResultFor('url', url);
    callbackRouterRemote = NewTabPageProxy.getInstance()
                               .callbackRouter.$.bindNewPipeAndPassRemote();
    backgroundManager = installMock(BackgroundManager);
    backgroundManager.setResultFor(
        'getBackgroundImageLoadTime', Promise.resolve(backgroundImageLoadTime));
    moduleRegistry = installMock(ModuleRegistry);
    moduleResolver = new PromiseResolver();
    moduleRegistry.setResultFor('initializeModules', moduleResolver.promise);
    metrics = fakeMetricsPrivate();

    app = document.createElement('ntp-app');
    document.body.appendChild(app);
  });

  suite('Misc', () => {
    test('logs height', async () => {
      // Assert.
      assertEquals(1, metrics.count('NewTabPage.Height'));
      assertEquals(
          1,
          metrics.count('NewTabPage.Height', Math.floor(window.innerHeight)));
    });

    test('logs width', async () => {
      // Assert.
      assertEquals(1, metrics.count('NewTabPage.Width'));
      assertEquals(
          1, metrics.count('NewTabPage.Width', Math.floor(window.innerWidth)));
    });

    test('open voice search event opens voice search overlay', async () => {
      // Act.
      $$(app, '#searchbox')!.dispatchEvent(new Event('open-voice-search'));
      await microtasksFinished();

      // Assert.
      assertTrue(!!app.shadowRoot!.querySelector('ntp-voice-search-overlay'));
      assertEquals(1, metrics.count('NewTabPage.VoiceActions'));
      assertEquals(
          1,
          metrics.count(
              'NewTabPage.VoiceActions', VoiceAction.ACTIVATE_SEARCH_BOX));
    });

    test('voice search keyboard shortcut', async () => {
      // Test correct shortcut opens voice search.
      // Act.
      window.dispatchEvent(new KeyboardEvent('keydown', {
        ctrlKey: true,
        shiftKey: true,
        code: 'Period',
      }));
      await microtasksFinished();

      // Assert.
      assertTrue(!!app.shadowRoot!.querySelector('ntp-voice-search-overlay'));
      assertEquals(1, metrics.count('NewTabPage.VoiceActions'));
      assertEquals(
          1,
          metrics.count(
              'NewTabPage.VoiceActions', VoiceAction.ACTIVATE_KEYBOARD));

      // Test other shortcut doesn't close voice search.
      // Act
      window.dispatchEvent(new KeyboardEvent('keydown', {
        ctrlKey: true,
        shiftKey: true,
        code: 'Enter',
      }));
      await microtasksFinished();

      // Assert.
      assertTrue(!!app.shadowRoot!.querySelector('ntp-voice-search-overlay'));
    });

    if (isMac) {
      test('keyboard shortcut opens voice search overlay on mac', async () => {
        // Act.
        window.dispatchEvent(new KeyboardEvent('keydown', {
          metaKey: true,
          shiftKey: true,
          code: 'Period',
        }));
        await microtasksFinished();

        // Assert.
        assertTrue(!!app.shadowRoot!.querySelector('ntp-voice-search-overlay'));
      });
    }

    test('help bubble can correctly find anchor elements', async () => {
      assertDeepEquals(
          app.getSortedAnchorStatusesForTesting(),
          [
            [CUSTOMIZE_CHROME_BUTTON_ELEMENT_ID, true],
          ],
      );
    });

    test('Webstore toast works correctly', async () => {
      const webstoreToast = $$<CrToastElement>(app, '#webstoreToast')!;
      assertTrue(webstoreToast.hidden);

      // Try to show webstore toast without opening side panel.
      callbackRouterRemote.showWebstoreToast();
      await callbackRouterRemote.$.flushForTesting();

      // The webstore toast should still be hidden.
      assertTrue(webstoreToast.hidden);
      assertFalse(webstoreToast.open);

      // Open the side panel.
      callbackRouterRemote.setCustomizeChromeSidePanelVisibility(true);
      await callbackRouterRemote.$.flushForTesting();

      // Try to show webstore toast again.
      callbackRouterRemote.showWebstoreToast();
      await callbackRouterRemote.$.flushForTesting();

      // The webstore toast should be open.
      assertFalse(webstoreToast.hidden);
      assertTrue(webstoreToast.open);
      assertTrue(!!webstoreToast.firstChild!.textContent);
    });
  });

  suite(`OgbThemingRemoveScrim`, () => {
    test('Ogb updates on ntp load', async () => {
      // Act.

      // Create a dark mode theme with a custom background.
      const theme = createTheme(true);
      theme.backgroundImage = createBackgroundImage('https://foo.com');
      callbackRouterRemote.setTheme(theme);
      await callbackRouterRemote.$.flushForTesting();

      // Notify the NTP that the ogb has loaded.
      window.dispatchEvent(new MessageEvent('message', {
        data: {
          frameType: 'one-google-bar',
          messageType: 'loaded',
        },
        source: window,
        origin: window.origin,
      }));
      await microtasksFinished();

      // Assert.

      // Dark mode themes with background images and removeScrim set should
      // apply background protection to the ogb.
      assertEquals(1, windowProxy.getCallCount('postMessage'));
      const [_, {type, applyLightTheme}] =
          windowProxy.getArgs('postMessage')[0];
      assertEquals('updateAppearance', type);
      assertEquals(true, applyLightTheme);
      assertNotStyle($$(app, '#oneGoogleBarScrim')!, 'display', 'none');
    });
  });

  suite('OgbScrim', () => {
    test('scroll bounce', async () => {
      // Arrange.

      // Set theme that triggers the scrim.
      const theme = createTheme(true);
      theme.backgroundImage = createBackgroundImage('https://foo.com');
      callbackRouterRemote.setTheme(theme);
      await callbackRouterRemote.$.flushForTesting();

      // Make sure page is scrollable.
      const spacer = document.createElement('div');
      spacer.style.width = '100%';
      spacer.style.height = '10000px';
      spacer.style.flexShrink = '0';
      $$(app, '#content')!.append(spacer);

      // Simulates a vertical scroll.
      const scrollY = async (y: number) => {
        window.scroll(0, y);
        // `window.scroll` doesn't automatically trigger scroll event.
        window.dispatchEvent(new Event('scroll'));
        // Wait for position update to propagate.
        await new Promise<void>(
            resolve => requestAnimationFrame(() => resolve()));
      };

      // Act (no bounce).
      await scrollY(0);

      // Assert (no bounce).
      assertStyle($$(app, '#oneGoogleBarScrim')!, 'position', 'fixed');

      // Act (scroll).
      await scrollY(10);

      // Assert (scroll).
      assertStyle($$(app, '#oneGoogleBarScrim')!, 'position', 'absolute');
    });
  });

  suite('Theming', () => {
    test('setting theme updates ntp', async () => {
      // Act.
      callbackRouterRemote.setTheme(createTheme());
      await callbackRouterRemote.$.flushForTesting();
      await microtasksFinished();

      // Assert.
      assertEquals(1, backgroundManager.getCallCount('setBackgroundColor'));
      assertEquals(
          0xffff0000 /* red */,
          (await backgroundManager.whenCalled('setBackgroundColor')).value);
      assertStyle(
          $$(app, '#content')!, '--color-new-tab-page-attribution-foreground',
          'rgba(0, 0, 255, 1.00)');
      assertEquals(1, backgroundManager.getCallCount('setShowBackgroundImage'));
      assertFalse(await backgroundManager.whenCalled('setShowBackgroundImage'));
      assertStyle($$(app, '#backgroundImageAttribution')!, 'display', 'none');
      assertStyle($$(app, '#backgroundImageAttribution2')!, 'display', 'none');
      assertFalse(app.$.logo.singleColored);
      assertFalse(app.$.logo.dark);
      assertEquals(0xffff0000, app.$.logo.backgroundColor.value);
    });

    test('setting 3p theme shows attribution', async () => {
      // Arrange.
      const theme = createTheme();
      theme.backgroundImage = createBackgroundImage('https://foo.com');
      theme.backgroundImage.attributionUrl = {url: 'chrome://theme/foo'};

      // Act.
      callbackRouterRemote.setTheme(theme);
      await callbackRouterRemote.$.flushForTesting();
      await microtasksFinished();

      assertNotStyle($$(app, '#themeAttribution')!, 'display', 'none');
      assertEquals(
          'chrome://theme/foo',
          $$<HTMLImageElement>(app, '#themeAttribution img')!.src);
    });

    test('setting background image shows image', async () => {
      // Arrange.
      const theme = createTheme();
      theme.backgroundImage = createBackgroundImage('https://img.png');

      // Act.
      backgroundManager.resetResolver('setShowBackgroundImage');
      callbackRouterRemote.setTheme(theme);
      await callbackRouterRemote.$.flushForTesting();

      // Assert.
      assertEquals(1, backgroundManager.getCallCount('setShowBackgroundImage'));
      assertTrue(await backgroundManager.whenCalled('setShowBackgroundImage'));

      // Scrim removal will remove text shadows as background protection is
      // applied to the background element instead.
      assertNotStyle(
          $$(app, '#backgroundImageAttribution')!, 'background-color',
          'rgba(0, 0, 0, 0)');
      assertStyle(
          $$(app, '#backgroundImageAttribution')!, 'text-shadow', 'none');

      assertEquals(1, backgroundManager.getCallCount('setBackgroundImage'));
      assertEquals(
          'https://img.png',
          (await backgroundManager.whenCalled('setBackgroundImage')).url.url);
      assertEquals(null, app.$.logo.backgroundColor);
    });

    test('setting attributions shows attributions', async function() {
      // Arrange.
      const theme = createTheme();
      theme.backgroundImageAttribution1 = 'foo';
      theme.backgroundImageAttribution2 = 'bar';
      theme.backgroundImageAttributionUrl = {url: 'https://info.com'};

      // Act.
      callbackRouterRemote.setTheme(theme);
      await callbackRouterRemote.$.flushForTesting();

      // Assert.
      assertNotStyle(
          $$(app, '#backgroundImageAttribution')!, 'display', 'none');
      assertNotStyle(
          $$(app, '#backgroundImageAttribution2')!, 'display', 'none');
      assertEquals(
          'https://info.com',
          $$(app, '#backgroundImageAttribution')!.getAttribute('href'));
      assertEquals(
          'foo', $$(app, '#backgroundImageAttribution1')!.textContent!.trim());
      assertEquals(
          'bar', $$(app, '#backgroundImageAttribution2')!.textContent!.trim());
    });

    test('setting logo color colors logo', async function() {
      // Arrange.
      const theme = createTheme();
      theme.logoColor = {value: 0xffff0000};

      // Act.
      callbackRouterRemote.setTheme(theme);
      await callbackRouterRemote.$.flushForTesting();

      // Assert.
      assertTrue(app.$.logo.singleColored);
      assertStyle(app.$.logo, '--ntp-logo-color', 'rgba(255, 0, 0, 1.00)');
    });

    test('theme updates add shortcut color', async () => {
      const theme = createTheme();
      theme.mostVisited.useWhiteTileIcon = true;
      callbackRouterRemote.setTheme(theme);
      const mostVisited = $$(app, '#mostVisited');
      assertTrue(!!mostVisited);
      assertFalse(mostVisited.hasAttribute('use-white-tile-icon_'));
      await callbackRouterRemote.$.flushForTesting();
      assertTrue(mostVisited.hasAttribute('use-white-tile-icon_'));
    });

    test('theme updates is dark', async () => {
      const theme = createTheme();
      theme.mostVisited.isDark = true;
      callbackRouterRemote.setTheme(theme);
      const mostVisited = $$(app, '#mostVisited');
      assertTrue(!!mostVisited);
      assertFalse(mostVisited.hasAttribute('is-dark_'));
      await callbackRouterRemote.$.flushForTesting();
      assertTrue(mostVisited.hasAttribute('is-dark_'));
    });

    [true, false].forEach((isDark) => {
      test(
          `OGB light mode whenever background image
          (ignoring dark mode) isDark: ${isDark}`,
          async () => {
            // Act.

            // Create a theme with a custom background.
            const theme = createTheme(isDark);
            theme.backgroundImage = createBackgroundImage('https://foo.com');
            callbackRouterRemote.setTheme(theme);
            await callbackRouterRemote.$.flushForTesting();

            // Notify the NTP that the ogb has loaded.
            window.dispatchEvent(new MessageEvent('message', {
              data: {
                frameType: 'one-google-bar',
                messageType: 'loaded',
              },
              source: window,
              origin: window.origin,
            }));
            await microtasksFinished();

            // Assert.
            assertEquals(1, windowProxy.getCallCount('postMessage'));
            const [_, {type, applyLightTheme}] =
                windowProxy.getArgs('postMessage')[0];
            assertEquals('updateAppearance', type);
            assertEquals(true, applyLightTheme);
          });
    });

    suite('theming metrics', () => {
      test('having no theme produces correct metrics', async () => {
        // Arrange.
        const theme = createTheme();
        theme.isCustomBackground = false;

        // Act.
        callbackRouterRemote.setTheme(theme);
        await callbackRouterRemote.$.flushForTesting();

        // Assert.
        assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad'));
        assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad', ''));
        assertEquals(1, metrics.count('NewTabPage.BackgroundImageSource'));
        assertEquals(
            1,
            metrics.count(
                'NewTabPage.BackgroundImageSource',
                NtpBackgroundImageSource.kNoImage));
      });

      test('having first party theme produces correct metrics', async () => {
        // Arrange.
        const theme = createTheme();
        theme.backgroundImage = createBackgroundImage('https://foo.com');
        theme.backgroundImage.imageSource =
            NtpBackgroundImageSource.kFirstPartyThemeWithoutDailyRefresh;
        theme.backgroundImageCollectionId = 'foo_collection';

        // Act.
        callbackRouterRemote.setTheme(theme);
        await callbackRouterRemote.$.flushForTesting();

        // Assert.
        assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad'));
        assertEquals(
            1,
            metrics.count('NewTabPage.Collections.IdOnLoad', 'foo_collection'));
        assertEquals(1, metrics.count('NewTabPage.BackgroundImageSource'));
        assertEquals(
            1,
            metrics.count(
                'NewTabPage.BackgroundImageSource',
                NtpBackgroundImageSource.kFirstPartyThemeWithoutDailyRefresh));
      });

      test('having third party theme produces correct metrics', async () => {
        // Arrange.
        const theme = createTheme();
        theme.backgroundImage = createBackgroundImage('https://foo.com');
        theme.backgroundImage.imageSource =
            NtpBackgroundImageSource.kThirdPartyTheme;

        // Act.
        callbackRouterRemote.setTheme(theme);
        await callbackRouterRemote.$.flushForTesting();

        // Assert.
        assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad'));
        assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad', ''));
        assertEquals(1, metrics.count('NewTabPage.BackgroundImageSource'));
        assertEquals(
            1,
            metrics.count(
                'NewTabPage.BackgroundImageSource',
                NtpBackgroundImageSource.kThirdPartyTheme));
      });

      test(
          'having refresh daily enabled produces correct metrics', async () => {
            // Arrange.
            const theme = createTheme();
            theme.backgroundImage = createBackgroundImage('https://foo.com');
            theme.backgroundImage.imageSource =
                NtpBackgroundImageSource.kFirstPartyThemeWithDailyRefresh;
            theme.backgroundImageCollectionId = 'foo_collection';

            // Act.
            callbackRouterRemote.setTheme(theme);
            await callbackRouterRemote.$.flushForTesting();

            // Assert.
            assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad'));
            assertEquals(
                1,
                metrics.count(
                    'NewTabPage.Collections.IdOnLoad', 'foo_collection'));
            assertEquals(1, metrics.count('NewTabPage.BackgroundImageSource'));
            assertEquals(
                1,
                metrics.count(
                    'NewTabPage.BackgroundImageSource',
                    NtpBackgroundImageSource.kFirstPartyThemeWithDailyRefresh));
          });

      test('setting uploaded background produces correct metrics', async () => {
        // Arrange.
        const theme = createTheme();
        theme.backgroundImage = createBackgroundImage('https://foo.com');
        theme.backgroundImage.imageSource =
            NtpBackgroundImageSource.kUploadedImage;

        // Act.
        callbackRouterRemote.setTheme(theme);
        await callbackRouterRemote.$.flushForTesting();

        // Assert.
        assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad'));
        assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad', ''));
        assertEquals(1, metrics.count('NewTabPage.BackgroundImageSource'));
        assertEquals(
            1,
            metrics.count(
                'NewTabPage.BackgroundImageSource',
                NtpBackgroundImageSource.kUploadedImage));
      });

      test(
          'setting wallpaper search background produces correct metrics',
          async () => {
            // Arrange.
            const theme = createTheme();
            theme.backgroundImage = createBackgroundImage('https://foo.com');
            theme.backgroundImage.imageSource =
                NtpBackgroundImageSource.kWallpaperSearch;

            // Act.
            callbackRouterRemote.setTheme(theme);
            await callbackRouterRemote.$.flushForTesting();

            // Assert.
            assertEquals(1, metrics.count('NewTabPage.Collections.IdOnLoad'));
            assertEquals(
                1, metrics.count('NewTabPage.Collections.IdOnLoad', ''));
            assertEquals(1, metrics.count('NewTabPage.BackgroundImageSource'));
            assertEquals(
                1,
                metrics.count(
                    'NewTabPage.BackgroundImageSource',
                    NtpBackgroundImageSource.kWallpaperSearch));
          });

      suite('background image load', () => {
        suiteSetup(() => {
          loadTimeData.overrideValues({backgroundImageUrl: 'https://foo.com'});
        });

        test('background image load time is logged', async () => {
          // Assert.
          assertEquals(
              1, metrics.count('NewTabPage.Images.ShownTime.BackgroundImage'));
          assertEquals(
              1,
              metrics.count(
                  'NewTabPage.Images.ShownTime.BackgroundImage',
                  Math.floor(
                      backgroundImageLoadTime -
                      window.performance.timeOrigin)));
        });
      });
    });
  });

  suite('Promo', () => {
    test('can show promo with browser command', async () => {
      const promoBrowserCommandHandler = installMock(
          CommandHandlerRemote,
          mock => BrowserCommandProxy.getInstance().handler = mock);
      promoBrowserCommandHandler.setResultFor(
          'canExecuteCommand', Promise.resolve({canExecute: true}));

      const commandId = 123;  // Unsupported command.
      window.dispatchEvent(new MessageEvent('message', {
        data: {
          frameType: 'one-google-bar',
          messageType: 'can-show-promo-with-browser-command',
          commandId,
        },
        source: window,
        origin: window.origin,
      }));

      // Make sure the command is sent to the browser.
      const expectedCommandId =
          await promoBrowserCommandHandler.whenCalled('canExecuteCommand');
      // Unsupported commands get resolved to the default command before being
      // sent to the browser.
      assertEquals(Command.kUnknownCommand, expectedCommandId);

      // Make sure the promo frame gets notified whether the promo can be shown.
      const {data} = await eventToPromise('message', window);
      assertEquals('can-show-promo-with-browser-command', data.messageType);
      assertTrue(data[commandId]);
    });

    test('executes promo browser command', async () => {
      const promoBrowserCommandHandler = installMock(
          CommandHandlerRemote,
          mock => BrowserCommandProxy.getInstance().handler = mock);
      promoBrowserCommandHandler.setResultFor(
          'executeCommand', Promise.resolve({commandExecuted: true}));

      const commandId = 123;  // Unsupported command.
      const clickInfo = {middleButton: true};
      window.dispatchEvent(new MessageEvent('message', {
        data: {
          frameType: 'one-google-bar',
          messageType: 'execute-browser-command',
          data: {
            commandId,
            clickInfo,
          },
        },
        source: window,
        origin: window.origin,
      }));

      // Make sure the command and click information are sent to the browser.
      const [expectedCommandId, expectedClickInfo] =
          await promoBrowserCommandHandler.whenCalled('executeCommand');
      // Unsupported commands get resolved to the default command before being
      // sent to the browser.
      assertEquals(Command.kUnknownCommand, expectedCommandId);
      assertEquals(clickInfo, expectedClickInfo);

      // Make sure the promo frame gets notified whether the command was
      // executed.
      const {data: commandExecuted} = await eventToPromise('message', window);
      assertTrue(commandExecuted);
    });
  });

  suite('Clicks', () => {
    suiteSetup(() => {
      loadTimeData.overrideValues({
        modulesEnabled: true,
      });
    });

    ([
      ['#content', NtpElement.BACKGROUND],
      ['ntp-logo', NtpElement.LOGO],
      ['cr-searchbox', NtpElement.REALBOX],
      ['cr-most-visited', NtpElement.MOST_VISITED],
      ['ntp-middle-slot-promo', NtpElement.MIDDLE_SLOT_PROMO],
      ['#modules', NtpElement.MODULE],
      ['#customizeButton', NtpElement.CUSTOMIZE_BUTTON],
    ] as Array<[string, NtpElement]>)
        .forEach(([selector, element]) => {
          test(`clicking '${selector}' records click`, () => {
            // Act.
            $$<HTMLElement>(app, selector)!.click();

            // Assert.
            assertEquals(1, metrics.count('NewTabPage.Click'));
            assertEquals(1, metrics.count('NewTabPage.Click', element));
          });
        });

    test('clicking OGB records click', () => {
      // Act.
      window.dispatchEvent(new MessageEvent('message', {
        data: {
          frameType: 'one-google-bar',
          messageType: 'click',
        },
      }));

      // Assert.
      assertEquals(1, metrics.count('NewTabPage.Click'));
      assertEquals(
          1, metrics.count('NewTabPage.Click', NtpElement.ONE_GOOGLE_BAR));
    });
  });

  function modulesCommonTests(modulesElementTag: string) {
    test('promo and modules coordinate', async () => {
      // Arrange.
      loadTimeData.overrideValues({navigationStartTime: 0.0});
      windowProxy.setResultFor('now', 123.0);
      const middleSlotPromo = $$(app, 'ntp-middle-slot-promo');
      assertTrue(!!middleSlotPromo);
      const modules = $$(app, modulesElementTag)!;
      assertTrue(!!modules);

      // Assert.
      assertStyle(middleSlotPromo, 'display', 'none');
      assertStyle(modules, 'display', 'none');

      // Act.
      middleSlotPromo.dispatchEvent(new Event('ntp-middle-slot-promo-loaded'));

      // Assert.
      assertStyle(middleSlotPromo, 'display', 'none');
      assertStyle(modules, 'display', 'none');

      // Act.
      modules.dispatchEvent(new Event('modules-loaded'));
      await microtasksFinished();

      // Assert.
      assertNotStyle(middleSlotPromo, 'display', 'none');
      assertNotStyle(modules, 'display', 'none');
      assertEquals(1, metrics.count('NewTabPage.Modules.ShownTime'));
      assertEquals(1, metrics.count('NewTabPage.Modules.ShownTime', 123));
    });
  }

  suite('Modules', () => {
    suiteSetup(() => {
      loadTimeData.overrideValues({
        modulesEnabled: true,
        modulesRedesignedEnabled: false,
        wideModulesEnabled: false,
      });
    });

    [560, 672, 768].forEach(pageWidth => {
      test(
          `module width defaults to search box width rule applied for width: ${
              pageWidth}px`,
          () => {
            document.body.setAttribute('style', `width:${pageWidth}px`);
            const middleSlotPromo = $$(app, 'ntp-middle-slot-promo')!;
            middleSlotPromo.dispatchEvent(
                new Event('ntp-middle-slot-promo-loaded'));
            const modules = $$(app, 'ntp-modules')!;
            modules.dispatchEvent(new Event('modules-loaded'));
            const searchBoxWidth =
                window.getComputedStyle(app)
                    .getPropertyValue('--ntp-search-box-width')
                    .trim();

            assertStyle(modules, 'width', `${searchBoxWidth}`);
          });
    });

    test('modules max width media rule applied', async () => {
      const sampleMaxWidthPx = 768;
      loadTimeData.overrideValues({wideModulesEnabled: true});
      document.body.innerHTML = window.trustedTypes!.emptyHTML;
      document.body.setAttribute('style', `width:${sampleMaxWidthPx}px`);
      app = document.createElement('ntp-app');
      document.body.appendChild(app);
      await microtasksFinished();

      const middleSlotPromo = $$(app, 'ntp-middle-slot-promo')!;
      middleSlotPromo.dispatchEvent(new Event('ntp-middle-slot-promo-loaded'));
      const modules = $$(app, 'ntp-modules')!;
      modules.dispatchEvent(new Event('modules-loaded'));
      await microtasksFinished();

      assertStyle(modules, 'width', `${sampleMaxWidthPx}px`);
    });

    modulesCommonTests('ntp-modules');
  });

  suite('V2Modules', () => {
    suiteSetup(() => {
      loadTimeData.overrideValues({
        modulesEnabled: true,
        modulesRedesignedEnabled: true,
      });
    });

    test('container is hidden', async () => {
      const modules = $$(app, 'ntp-modules-v2')!;
      assertTrue(!!modules);
      assertStyle(modules, 'display', 'none');
    });

    test('modules redesigned attribute applied', async () => {
      assertTrue(app.hasAttribute('modules-redesigned-enabled_'));
    });

    test(`clicking records click`, () => {
      // Act.
      $$<HTMLElement>(app, 'ntp-modules-v2')!.click();

      // Assert.
      assertEquals(1, metrics.count('NewTabPage.Click'));
      assertEquals(1, metrics.count('NewTabPage.Click', NtpElement.MODULE));
    });

    modulesCommonTests('ntp-modules-v2');
  });

  suite('v2 modules', () => {
    suiteSetup(() => {
      loadTimeData.overrideValues({
        modulesEnabled: true,
        modulesRedesignedEnabled: true,
      });
    });

    test('container is hidden', async () => {
      const modules = $$(app, 'ntp-modules-v2')!;
      assertTrue(!!modules);
      assertStyle(modules, 'display', 'none');
    });

    test(`clicking records click`, () => {
      // Act.
      $$<HTMLElement>(app, 'ntp-modules-v2')!.click();

      // Assert.
      assertEquals(1, metrics.count('NewTabPage.Click'));
      assertEquals(1, metrics.count('NewTabPage.Click', NtpElement.MODULE));
    });
  });

  suite('CounterfactualModules', () => {
    suiteSetup(() => {
      loadTimeData.overrideValues({
        modulesEnabled: false,
        modulesLoadEnabled: true,
      });
    });

    test('modules loaded but not rendered if counterfactual', async () => {
      // Act.
      const fooElement = document.createElement('div');
      const barElement = document.createElement('div');
      moduleResolver.resolve([
        {
          descriptor:
              new ModuleDescriptor('foo', () => Promise.resolve(fooElement)),
          elements: [fooElement],
        },
        {
          descriptor:
              new ModuleDescriptor('bar', () => Promise.resolve(barElement)),
          elements: [barElement],
        },
      ]);
      await counterfactualLoad();
      await microtasksFinished();

      // Assert.
      assertTrue(moduleRegistry.getCallCount('initializeModules') > 0);
      assertEquals(1, handler.getCallCount('onModulesLoadedWithData'));
      assertEquals(
          0, app.shadowRoot!.querySelectorAll('ntp-module-wrapper').length);
    });
  });

  suite('CustomizeChromeSidePanel', () => {
    test('customize chrome in product help might show on startup'), () => {
      assertEquals(1, handler.getCallCount('maybeShowFeaturePromo'));
    };

    test('clicking customize button opens side panel', () => {
      // Act.
      $$<HTMLElement>(app, '#customizeButton')!.click();

      // Assert.
      assertDeepEquals(
          [true, CustomizeChromeSection.kUnspecified],
          handler.getArgs('setCustomizeChromeSidePanelVisible')[0]);
      assertEquals(
          1,
          metrics.count(
              'NewTabPage.CustomizeChromeOpened',
              NtpCustomizeChromeEntryPoint.CUSTOMIZE_BUTTON));
      assertEquals(
          1, handler.getCallCount('incrementCustomizeChromeButtonOpenCount'));
    });

    test('clicking customize button hides side panel', async () => {
      // Act.
      callbackRouterRemote.setCustomizeChromeSidePanelVisibility(true);
      assertEquals(
          0,
          metrics.count(
              'NewTabPage.CustomizeChromeOpened',
              NtpCustomizeChromeEntryPoint.CUSTOMIZE_BUTTON));
      await callbackRouterRemote.$.flushForTesting();
      $$<HTMLElement>(app, '#customizeButton')!.click();

      // Assert.
      assertDeepEquals(
          [false, CustomizeChromeSection.kUnspecified],
          handler.getArgs('setCustomizeChromeSidePanelVisible')[0]);
      assertEquals(
          0,
          metrics.count(
              'NewTabPage.CustomizeChromeOpened',
              NtpCustomizeChromeEntryPoint.CUSTOMIZE_BUTTON));
      assertEquals(
          0, handler.getCallCount('incrementCustomizeChromeButtonOpenCount'));
    });

    test('clicking customize button is accessible', async () => {
      callbackRouterRemote.setCustomizeChromeSidePanelVisibility(true);
      await callbackRouterRemote.$.flushForTesting();
      assertEquals(
          'true',
          $$<HTMLElement>(
              app, '#customizeButton')!.getAttribute('aria-pressed'));
      callbackRouterRemote.setCustomizeChromeSidePanelVisibility(false);
      await callbackRouterRemote.$.flushForTesting();
      assertEquals(
          'false',
          $$<HTMLElement>(
              app, '#customizeButton')!.getAttribute('aria-pressed'));
    });

    suite('modules', () => {
      suiteSetup(() => {
        loadTimeData.overrideValues({
          modulesEnabled: true,
        });
      });

      test('modules can open side panel', async () => {
        // Act.
        $$(app, '#modules')!.dispatchEvent(new Event('customize-module'));

        // Assert.
        assertDeepEquals(
            [true, CustomizeChromeSection.kModules],
            handler.getArgs('setCustomizeChromeSidePanelVisible')[0]);
        assertEquals(
            1,
            metrics.count(
                'NewTabPage.CustomizeChromeOpened',
                NtpCustomizeChromeEntryPoint.MODULE));
      });
    });

    suite('customize URL', () => {
      suiteSetup(() => {
        // We inject the URL param in this suite setup so that the URL is
        // updated before the app element gets created.
        url.searchParams.append('customize', CustomizeDialogPage.THEMES);
      });

      test('URL opens side panel', () => {
        // Assert.
        assertDeepEquals(
            [true, CustomizeChromeSection.kAppearance],
            handler.getArgs('setCustomizeChromeSidePanelVisible')[0]);
        assertEquals(
            1,
            metrics.count(
                'NewTabPage.CustomizeChromeOpened',
                NtpCustomizeChromeEntryPoint.URL));
      });
    });
  });

  suite('LensUploadDialog', () => {
    suiteSetup(() => {
      loadTimeData.overrideValues({
        searchboxLensSearch: true,
      });
    });

    test('lens upload dialog closed on start', () => {
      // Assert.
      assertFalse(!!app.shadowRoot!.querySelector('ntp-lens-upload-dialog'));
    });

    test('realbox is not visible when Lens upload dialog is open', async () => {
      // Arrange.
      callbackRouterRemote.setTheme(createTheme());
      await callbackRouterRemote.$.flushForTesting();

      // Act.
      $$(app, '#searchbox')!.dispatchEvent(new Event('open-lens-search'));
      await microtasksFinished();

      // Assert.
      const dialog = app.shadowRoot!.querySelector('ntp-lens-upload-dialog');
      assertTrue(!!dialog);
      assertStyle($$(app, '#searchbox')!, 'visibility', 'hidden');

      // Act.
      dialog.closeDialog();
      await microtasksFinished();

      // Assert.
      assertStyle($$(app, '#searchbox')!, 'visibility', 'visible');
    });
  });

  suite('WallpaperSearch', () => {
    suite('ButtonDisabled', () => {
      suiteSetup(() => {
        loadTimeData.overrideValues({
          wallpaperSearchButtonEnabled: false,
        });
      });

      test('does not increment button shown count on startup'), () => {
        assertEquals(
            0,
            handler.getCallCount('incrementWallpaperSearchButtonShownCount'));
      };

      test('wallpaper search button is not shown if it is disabled', () => {
        assertTrue(!!app.shadowRoot!.querySelector('#customizeButton'));
        assertFalse(!!app.shadowRoot!.querySelector('#wallpaperSearchButton'));
      });

      test(
          'setting background image styles customize chrome button',
          async () => {
            // Customize chrome button is expanded and its icon has a
            // non-white color.
            assertNotEquals(
                32, $$<HTMLElement>(app, '#customizeButton')!.offsetWidth);
            assertNotStyle(
                $$(app, '#customizeButton .customize-text')!, 'display',
                'none');
            assertNotStyle(
                $$(app, '#customizeButton .customize-icon')!,
                'background-color', 'rgb(255, 255, 255)');

            const theme = createTheme(true);
            theme.backgroundImage = createBackgroundImage('https://foo.com');
            callbackRouterRemote.setTheme(theme);
            await callbackRouterRemote.$.flushForTesting();

            // Customize chrome button is collapsed and its icon is white.
            assertEquals(
                32, $$<HTMLElement>(app, '#customizeButton')!.offsetWidth);
            assertStyle(
                $$(app, '#customizeButton .customize-icon')!,
                'background-color', 'rgb(255, 255, 255)');
            assertStyle(
                $$(app, '#customizeButton .customize-text')!, 'display',
                'none');
          });
    });

    function assertButtonAnimated() {
      assertNotStyle(
          $$(app, '#wallpaperSearchButton')!, 'animation-name', 'none');
      assertNotStyle(
          $$(app, '#wallpaperSearchButton .customize-icon')!, 'animation-name',
          'none');
      assertStyle(
          $$(app, '#wallpaperSearchButton .customize-text')!, 'animation-name',
          'none');
    }

    function assertButtonNotAnimated() {
      assertStyle($$(app, '#wallpaperSearchButton')!, 'animation-name', 'none');
      assertStyle(
          $$(app, '#wallpaperSearchButton .customize-icon')!, 'animation-name',
          'none');
      assertStyle(
          $$(app, '#wallpaperSearchButton .customize-text')!, 'animation-name',
          'none');
    }

    suite('ButtonEnabled', () => {
      suiteSetup(() => {
        loadTimeData.overrideValues({
          wallpaperSearchButtonEnabled: true,
          wallpaperSearchButtonAnimationEnabled: true,
        });
      });

      test('increments button shown count on startup'), () => {
        assertEquals(
            1,
            handler.getCallCount('incrementWallpaperSearchButtonShownCount'));
      };

      test('wallpaper search button shows if it is enabled', () => {
        assertTrue(!!app.shadowRoot!.querySelector('#customizeButton'));
        assertTrue(!!app.shadowRoot!.querySelector('#wallpaperSearchButton'));
      });

      test('button has animation', () => {
        assertButtonAnimated();
      });

      [NtpBackgroundImageSource.kWallpaperSearch,
       NtpBackgroundImageSource.kWallpaperSearchInspiration]
          .forEach((imageSource) => {
            test(
                `having wallpaper search theme ${
                    imageSource} disables animation`,
                async () => {
                  // Arrange.
                  const theme = createTheme();
                  theme.backgroundImage =
                      createBackgroundImage('https://foo.com');
                  theme.backgroundImage.imageSource = imageSource;
                  assertButtonAnimated();

                  // Act.
                  callbackRouterRemote.setTheme(theme);
                  await callbackRouterRemote.$.flushForTesting();

                  // Assert.
                  assertButtonNotAnimated();
                });
          });

      ([
        ['#customizeButton', NtpElement.CUSTOMIZE_BUTTON],
        ['#wallpaperSearchButton', NtpElement.WALLPAPER_SEARCH_BUTTON],
      ] as Array<[string, NtpElement]>)
          .forEach(([selector, element]) => {
            test(`clicking #wallpaperSearchButton records click`, () => {
              $$<HTMLElement>(app, selector)!.click();

              assertEquals(1, metrics.count('NewTabPage.Click'));
              assertEquals(1, metrics.count('NewTabPage.Click', element));
            });
          });

      test('clicking wallpaper search button opens side panel', () => {
        $$<HTMLElement>(app, '#wallpaperSearchButton')!.click();

        assertDeepEquals(
            [true, CustomizeChromeSection.kWallpaperSearch],
            handler.getArgs('setCustomizeChromeSidePanelVisible')[0]);
        assertEquals(
            1,
            metrics.count(
                'NewTabPage.CustomizeChromeOpened',
                NtpCustomizeChromeEntryPoint.WALLPAPER_SEARCH_BUTTON));
        assertEquals(
            1, handler.getCallCount('incrementCustomizeChromeButtonOpenCount'));
      });

      test(
          'wallpaper search button can open wallpaper search ' +
              'and hide side panel',
          async () => {
            // Open side panel to non-wallpaper search page.
            callbackRouterRemote.setCustomizeChromeSidePanelVisibility(true);
            assertEquals(
                0,
                metrics.count(
                    'NewTabPage.CustomizeChromeOpened',
                    NtpCustomizeChromeEntryPoint.WALLPAPER_SEARCH_BUTTON));
            await callbackRouterRemote.$.flushForTesting();

            // Clicking the wallpaper search button should navigate the side
            // panel to the wallpaper search page.
            $$<HTMLElement>(app, '#wallpaperSearchButton')!.click();

            assertDeepEquals(
                [true, CustomizeChromeSection.kWallpaperSearch],
                handler.getArgs('setCustomizeChromeSidePanelVisible')[0]);

            // Clicking the wallpaper search button, when the wallpaper search
            // page is opened, should close the side panel.
            $$<HTMLElement>(app, '#wallpaperSearchButton')!.click();

            assertDeepEquals(
                [false, CustomizeChromeSection.kUnspecified],
                handler.getArgs('setCustomizeChromeSidePanelVisible')[1]);
          });

      test('wallpaper search button is accessible', async () => {
        // Open side panel to non-wallpaper search page.
        callbackRouterRemote.setCustomizeChromeSidePanelVisibility(true);
        await callbackRouterRemote.$.flushForTesting();

        // Only customize chrome button should be labeled as pressed.
        assertEquals(
            'false',
            $$<HTMLElement>(
                app, '#wallpaperSearchButton')!.getAttribute('aria-pressed'));
        assertEquals(
            'true',
            $$<HTMLElement>(
                app, '#customizeButton')!.getAttribute('aria-pressed'));

        // Open wallpaper search page.
        $$<HTMLElement>(app, '#wallpaperSearchButton')!.click();
        await microtasksFinished();

        // Both buttons should be labeled as pressed.
        assertEquals(
            'true',
            $$<HTMLElement>(
                app, '#wallpaperSearchButton')!.getAttribute('aria-pressed'));
        assertEquals(
            'true',
            $$<HTMLElement>(
                app, '#customizeButton')!.getAttribute('aria-pressed'));

        // Close the side panel.
        callbackRouterRemote.setCustomizeChromeSidePanelVisibility(false);
        await callbackRouterRemote.$.flushForTesting();

        // Both buttons should not be labeled as pressed.
        assertEquals(
            'false',
            $$<HTMLElement>(
                app, '#wallpaperSearchButton')!.getAttribute('aria-pressed'));
        assertEquals(
            'false',
            $$<HTMLElement>(
                app, '#customizeButton')!.getAttribute('aria-pressed'));
      });

      test(
          'clicking wallpaper search button collapses/expands it', async () => {
            assertNotEquals(
                32,
                $$<HTMLElement>(app, '#wallpaperSearchButton')!.offsetWidth);
            assertNotStyle(
                $$(app, '#wallpaperSearchButton .customize-text')!, 'display',
                'none');

            $$<HTMLElement>(app, '#wallpaperSearchButton')!.click();
            await microtasksFinished();

            assertEquals(
                32,
                $$<HTMLElement>(app, '#wallpaperSearchButton')!.offsetWidth);
            assertStyle(
                $$(app, '#wallpaperSearchButton .customize-text')!, 'display',
                'none');
          });

      test(
          'setting background styles both customize chrome buttons',
          async () => {
            // Both buttons' icons should have a non-white color.
            assertNotStyle(
                $$<HTMLElement>(app, '#wallpaperSearchButton .customize-icon')!,
                'background-color', 'rgb(255, 255, 255)');
            assertNotStyle(
                $$<HTMLElement>(app, '#customizeButton .customize-icon')!,
                'background-color', 'rgb(255, 255, 255)');
            // Only customize chrome button should be collapsed.
            assertNotStyle(
                $$(app, '#wallpaperSearchButton .customize-text')!, 'display',
                'none');
            assertStyle(
                $$(app, '#customizeButton .customize-text')!, 'display',
                'none');
            assertNotEquals(
                32,
                $$<HTMLElement>(app, '#wallpaperSearchButton')!.offsetWidth);
            assertEquals(
                32, $$<HTMLElement>(app, '#customizeButton')!.offsetWidth);

            // Create and set theme.
            const theme = createTheme(true);
            theme.backgroundImage = createBackgroundImage('https://foo.com');
            callbackRouterRemote.setTheme(theme);
            await callbackRouterRemote.$.flushForTesting();

            // Both buttons' icons should be white.
            assertStyle(
                $$(app, '#wallpaperSearchButton .customize-icon')!,
                'background-color', 'rgb(255, 255, 255)');
            assertStyle(
                $$(app, '#customizeButton .customize-icon')!,
                'background-color', 'rgb(255, 255, 255)');
            // Only customize chrome button should be collapsed.
            assertNotStyle(
                $$(app, '#wallpaperSearchButton .customize-text')!, 'display',
                'none');
            assertStyle(
                $$(app, '#customizeButton .customize-text')!, 'display',
                'none');
            assertNotEquals(
                32,
                $$<HTMLElement>(app, '#wallpaperSearchButton')!.offsetWidth);
            assertEquals(
                32, $$<HTMLElement>(app, '#customizeButton')!.offsetWidth);
          });

      test(
          'button hides in accordance with callback router', async () => {
            // Both buttons shown.
            assertNotStyle($$(app, '#customizeButton')!, 'display', 'none');
            assertNotStyle(
                $$(app, '#wallpaperSearchButton')!, 'display', 'none');

            callbackRouterRemote.setWallpaperSearchButtonVisibility(false);
            await callbackRouterRemote.$.flushForTesting();
            await microtasksFinished();

            // Wallpaper search button hides.
            assertNotStyle($$(app, '#customizeButton')!, 'display', 'none');
            assertEquals(null, $$(app, '#wallpaperSearchButton'));

            callbackRouterRemote.setWallpaperSearchButtonVisibility(true);
            await callbackRouterRemote.$.flushForTesting();
            await microtasksFinished();

            // Wallpaper search button remains hidden.
            assertNotStyle($$(app, '#customizeButton')!, 'display', 'none');
            assertEquals(null, $$(app, '#wallpaperSearchButton'));
          });
    });

    suite('AnimationDisabled', () => {
      suiteSetup(() => {
        loadTimeData.overrideValues({
          wallpaperSearchButtonEnabled: true,
          wallpaperSearchButtonAnimationEnabled: false,
        });
      });

      test('button has no animation if the flag is disabled', () => {
        assertButtonNotAnimated();
      });
    });
  });
});