chromium/chrome/test/data/webui/new_tab_page/modules/v2/modules_test.ts

// Copyright 2023 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, ModuleWrapperElement, NamedWidth} from 'chrome://new-tab-page/lazy_load.js';
import {MODULE_CUSTOMIZE_ELEMENT_ID, ModuleDescriptor, ModuleRegistry, ModulesV2Element, SUPPORTED_MODULE_WIDTHS} from 'chrome://new-tab-page/lazy_load.js';
import {NewTabPageProxy} from 'chrome://new-tab-page/new_tab_page.js';
import type {PageRemote} from 'chrome://new-tab-page/new_tab_page.mojom-webui.js';
import {PageCallbackRouter, PageHandlerRemote} from 'chrome://new-tab-page/new_tab_page.mojom-webui.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {assertDeepEquals, assertEquals, assertFalse, 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 {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import type {TestMock} from 'chrome://webui-test/test_mock.js';

import {assertNotStyle, assertStyle, createElement, initNullModule, installMock} from '../../test_support.js';

const SAMPLE_SCREEN_WIDTH = 1080;
const MAX_COLUMN_COUNT = 5;
const NO_MAX_INSTANCE_COUNT = -1;

suite('NewTabPageModulesModulesV2Test', () => {
  let callbackRouterRemote: PageRemote;
  let handler: TestMock<PageHandlerRemote>;
  let metrics: MetricsTracker;
  let moduleRegistry: TestMock<ModuleRegistry>;

  setup(async () => {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    loadTimeData.overrideValues({
      modulesMaxColumnCount: MAX_COLUMN_COUNT,
      multipleLoadedModulesMaxModuleInstanceCount: NO_MAX_INSTANCE_COUNT,
    });
    handler = installMock(
        PageHandlerRemote,
        (mock: PageHandlerRemote) =>
            NewTabPageProxy.setInstance(mock, new PageCallbackRouter()));
    metrics = fakeMetricsPrivate();
    moduleRegistry = installMock(ModuleRegistry);
    callbackRouterRemote = NewTabPageProxy.getInstance()
                               .callbackRouter.$.bindNewPipeAndPassRemote();
  });

  async function createModulesElement(
      modules: Module[], enabled: boolean,
      width: number): Promise<ModulesV2Element> {
    if (!enabled) {
      assertTrue(
          modules.length === 0,
          'modules array must be empty if modules disabled');
    }
    const modulesPromise = new Promise<Module[]>((resolve, _) => {
      callbackRouterRemote.setDisabledModules(!enabled, []);
      callbackRouterRemote.$.flushForTesting().then(() => {
        resolve(modules);
      });
    });

    moduleRegistry.setResultFor('initializeModulesHavingIds', modulesPromise);
    const element = new ModulesV2Element();
    document.body.style.width = `${width}px`;
    document.body.appendChild(element);
    await modulesPromise;
    return element;
  }

  async function createModulesElementFromDescriptors(
      descriptors: ModuleDescriptor[],
      instanceCount: number): Promise<HTMLElement> {
    handler.setResultFor('getModulesIdNames', {
      data: descriptors,
    });

    const modules: Module[] = descriptors.map(descriptor => {
      return {
        descriptor: descriptor,
        elements: Array(instanceCount).fill(0).map(_ => createElement()),
      } as Module;
    });
    const modulesElement =
        await createModulesElement(modules, true, SAMPLE_SCREEN_WIDTH);
    return modulesElement;
  }

  const NARROW_WIDTH = SUPPORTED_MODULE_WIDTHS[0]!;
  const MEDIUM_WIDTH = SUPPORTED_MODULE_WIDTHS[1]!;
  const WIDE_WIDTH = SUPPORTED_MODULE_WIDTHS[2]!;

  interface Scenario {
    width: number;
    count: number;
    rows: NamedWidth[][];
  }

  interface LayoutChangeScenario {
    setup: Array<{name: string, count: number}>;
    before: Scenario;
    after: Scenario;
  }

  function generateScenarioCompactName(scenario: Scenario): string {
    return `scenario: ${scenario.width}W-${scenario.count}I[${
        scenario.rows.map(row => `${row.length}C`)}]`;
  }

  [{
    // 312px (narrow) * 5 + 8px (gap) * 4 + 48px (margin) * 2
    width: 1688,
    count: MAX_COLUMN_COUNT,
    rows: [[
      NARROW_WIDTH,
      NARROW_WIDTH,
      NARROW_WIDTH,
      NARROW_WIDTH,
      NARROW_WIDTH,
    ]],
  },
   {
     // 312px (narrow) * 4 + 8px (gap) * 3 + 48px (margin) * 2
     width: 1368,
     count: MAX_COLUMN_COUNT,
     rows: [
       [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
       [WIDE_WIDTH],
     ],
   },
   {
     // 312px (narrow) * 3 + 8px (gap) * 2 + 48px (margin) * 2
     width: 1048,
     count: 3,
     rows: [[NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH]],
   },
   {
     // 360px (medium) * 2 + 8px (gap) + 48px (margin) * 2
     width: 856,
     count: 3,
     rows: [[MEDIUM_WIDTH, MEDIUM_WIDTH], [WIDE_WIDTH]],
   },
   {
     // 728px (wide) * 1, + 48px (margin) * 2
     width: 824,
     count: 1,
     rows: [[WIDE_WIDTH]],
   },
   {
     // 312px (narrow) * 2 + 8px (gap) + 48px (margin) * 2
     width: 728,
     count: 3,
     rows: [[NARROW_WIDTH, NARROW_WIDTH], [MEDIUM_WIDTH]],
   },
   {
     // 360 (medium) * 1 + 48px (margin) * 2
     width: 456,
     count: 3,
     rows: [[MEDIUM_WIDTH], [MEDIUM_WIDTH], [MEDIUM_WIDTH]],
   },
   {
     // 360 (medium) * 1 + 48px (margin) * 2
     width: 456,
     count: 1,
     rows: [[MEDIUM_WIDTH]],
   },
   {
     // 312 (narrow) * 1 + 48px (margin) * 2
     width: 408,
     count: 3,
     rows: [[NARROW_WIDTH], [NARROW_WIDTH], [NARROW_WIDTH]],
   },
   {
     // 312 (narrow) * 1 + 48px (margin) * 2
     width: 408,
     count: 1,
     rows: [[NARROW_WIDTH]],
   },
  ].forEach((scenario: Scenario) => {
    test(`Layout ${generateScenarioCompactName(scenario)}`, async () => {
      const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
      handler.setResultFor('getModulesIdNames', {
        data: [
          {id: fooDescriptor.id, name: fooDescriptor.id},
        ],
      });

      const modulesElement = await createModulesElement(
          [
            {
              descriptor: fooDescriptor,
              elements: Array(scenario.count).fill(0).map(_ => createElement()),
            },
          ],
          true, scenario.width);
      await waitAfterNextRender(modulesElement);

      const wrappers = modulesElement.shadowRoot!.querySelectorAll(
          'ntp-module-wrapper:not([hidden])');
      assertEquals(scenario.count, wrappers.length);

      let index = 0;
      scenario.rows.forEach((expectedRowWidths, i) => {
        expectedRowWidths.forEach((expectedWidth, j) => {
          const wrapper = wrappers[index]! as ModuleWrapperElement;
          const instance = wrapper.$.moduleElement.lastChild! as HTMLElement;
          assertEquals(expectedWidth.name, instance.getAttribute('format'));
          assertEquals(
              expectedWidth.value, wrapper.clientWidth,
              `Element at row ${i} column ${j}`);
          index++;
        });
      });
    });
  });

  test('No modules rendered when all disabled', async () => {
    const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
    const barDescriptor = new ModuleDescriptor('bar', initNullModule);
    handler.setResultFor('getModulesIdNames', {
      data: [
        {id: fooDescriptor.id, name: fooDescriptor.id},
        {id: barDescriptor.id, name: barDescriptor.id},
      ],
    });
    const modulesElement =
        await createModulesElement([], false, SAMPLE_SCREEN_WIDTH);
    await waitAfterNextRender(modulesElement);

    const moduleWrappers =
        modulesElement.shadowRoot!.querySelectorAll('ntp-module-wrapper');
    assertEquals(0, moduleWrappers.length);
    assertEquals(1, metrics.count('NewTabPage.Modules.LoadedModulesCount', 0));
    assertEquals(1, metrics.count('NewTabPage.Modules.InstanceCount', 0));
    assertEquals(
        1, metrics.count('NewTabPage.Modules.VisibleOnNTPLoad', false));
  });

  test(
      'module(s) with multiple element instances render correcly', async () => {
        const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
        const barDescriptor = new ModuleDescriptor('bar', initNullModule);
        handler.setResultFor('getModulesIdNames', {
          data: [
            {id: fooDescriptor.id, name: fooDescriptor.id},
            {id: barDescriptor.id, name: barDescriptor.id},
          ],
        });

        const modulesElement = await createModulesElement(
            [
              {
                descriptor: fooDescriptor,
                elements: Array(3).fill(0).map(_ => createElement()),
              },
              {
                descriptor: barDescriptor,
                elements: [createElement()],
              },
            ],
            true, SAMPLE_SCREEN_WIDTH);

        const moduleWrappers =
            modulesElement.shadowRoot!.querySelectorAll('ntp-module-wrapper');
        assertEquals(4, moduleWrappers.length);
        assertEquals(1, metrics.count('NewTabPage.Modules.LoadedModulesCount'));
        assertEquals(1, metrics.count('NewTabPage.Modules.InstanceCount', 4));
        assertEquals(
            1, metrics.count('NewTabPage.Modules.VisibleOnNTPLoad', true));

        // Assert metrics for module loaded with other modules.
        assertEquals(
            1, metrics.count('NewTabPage.Modules.LoadedWith.foo', 'bar'));
        assertEquals(
            0, metrics.count('NewTabPage.Modules.LoadedWith.foo', 'foo'));
        assertEquals(
            1, metrics.count('NewTabPage.Modules.LoadedWith.bar', 'foo'));
        assertEquals(
            0, metrics.count('NewTabPage.Modules.LoadedWith.bar', 'bar'));
      });

  test('help bubble can correctly find anchor elements', async () => {
    const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
    handler.setResultFor('getModulesIdNames', {
      data: [
        {id: fooDescriptor.id, name: fooDescriptor.id},
      ],
    });

    const modulesElement = await createModulesElement(
        [
          {
            descriptor: fooDescriptor,
            elements: [createElement()],
          },
        ],
        true, SAMPLE_SCREEN_WIDTH);

    assertDeepEquals(
        modulesElement.getSortedAnchorStatusesForTesting(),
        [
          [MODULE_CUSTOMIZE_ELEMENT_ID, true],
        ],
    );
  });

  test('modules maxium instance count works correctly', async () => {
    const SAMPLE_MAX_MODULE_INSTANCE_COUNT = 2;
    loadTimeData.overrideValues({
      modulesMaxColumnCount: MAX_COLUMN_COUNT,
      multipleLoadedModulesMaxModuleInstanceCount:
          SAMPLE_MAX_MODULE_INSTANCE_COUNT,
    });

    const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
    const barDescriptor = new ModuleDescriptor('bar', initNullModule);
    const descriptors = [
      fooDescriptor,
      barDescriptor,
    ];
    const modulesElement = await createModulesElementFromDescriptors(
        descriptors, SAMPLE_MAX_MODULE_INSTANCE_COUNT + 1);
    const moduleWrappers =
        modulesElement.shadowRoot!.querySelectorAll('ntp-module-wrapper');
    assertEquals(
        descriptors.length * SAMPLE_MAX_MODULE_INSTANCE_COUNT,
        moduleWrappers.length);
  });

  test('modules maxium instance capped to maximum column count', async () => {
    const SAMPLE_MAX_COLUMN_COUNT = 3;
    const SAMPLE_MAX_MODULE_INSTANCE_COUNT = 3;
    loadTimeData.overrideValues({
      modulesMaxColumnCount: SAMPLE_MAX_COLUMN_COUNT,
      multipleLoadedModulesMaxModuleInstanceCount:
          SAMPLE_MAX_MODULE_INSTANCE_COUNT,
    });

    const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
    const barDescriptor = new ModuleDescriptor('bar', initNullModule);
    const bazDescriptor = new ModuleDescriptor('baz', initNullModule);
    const descriptors = [
      fooDescriptor,
      barDescriptor,
      bazDescriptor,
    ];
    const modulesElement = await createModulesElementFromDescriptors(
        descriptors, SAMPLE_MAX_MODULE_INSTANCE_COUNT);
    const moduleWrappers =
        modulesElement.shadowRoot!.querySelectorAll('ntp-module-wrapper');
    assertEquals(SAMPLE_MAX_COLUMN_COUNT, moduleWrappers.length);
  });

  enum UndoStrategy {
    BUTTON_ACTIVATION = 'button activation',
    SHORTCUT_KEY = 'shortcut key',
  }

  [UndoStrategy.BUTTON_ACTIVATION, UndoStrategy.SHORTCUT_KEY].forEach(
      (undoStrategy: UndoStrategy) => {
        test(
            `modules can be disabled and restored via ${undoStrategy}`,
            async () => {
              // Arrange.
              const moduleId = 'foo';
              const fooDescriptor =
                  new ModuleDescriptor(moduleId, initNullModule);
              handler.setResultFor('getModulesIdNames', {
                data: [
                  {id: fooDescriptor.id, name: fooDescriptor.id},
                ],
              });
              const modulesElement = await createModulesElement(
                  [{
                    descriptor: fooDescriptor,
                    elements: [createElement()],
                  }],
                  true, SAMPLE_SCREEN_WIDTH);

              // Assert.
              const moduleWrappers =
                  modulesElement.shadowRoot!.querySelectorAll(
                      'ntp-module-wrapper');
              assertEquals(1, moduleWrappers.length);
              assertNotStyle(moduleWrappers[0]!, 'display', 'none');
              assertFalse(modulesElement.$.undoToast.open);

              // Act.
              let restoreCalled = false;
              moduleWrappers[0]!.dispatchEvent(
                  new CustomEvent('disable-module', {
                    bubbles: true,
                    composed: true,
                    detail: {
                      message: 'Foo',
                      restoreCallback: () => {
                        restoreCalled = true;
                      },
                    },
                  }));

              // Assert.
              assertDeepEquals(
                  ['foo', true], handler.getArgs('setModuleDisabled')[0]);

              // Act.
              callbackRouterRemote.setDisabledModules(false, [moduleId]);
              await callbackRouterRemote.$.flushForTesting();

              // Assert.
              assertStyle(moduleWrappers[0]!, 'display', 'none');
              assertTrue(modulesElement.$.undoToast.open);
              assertEquals(
                  'Foo', modulesElement.$.undoToastMessage.textContent!.trim());
              assertEquals(
                  1, metrics.count('NewTabPage.Modules.Disabled', moduleId));
              assertEquals(
                  1,
                  metrics.count(
                      'NewTabPage.Modules.Disabled.ModuleRequest', moduleId));
              assertFalse(restoreCalled);

              // Act.
              await waitAfterNextRender(modulesElement);
              if (undoStrategy === UndoStrategy.BUTTON_ACTIVATION) {
                const undoButton =
                    modulesElement.shadowRoot!.querySelector<HTMLElement>(
                        '#undoButton');
                assertTrue(!!undoButton);
                undoButton.click();
              } else if (undoStrategy === UndoStrategy.SHORTCUT_KEY) {
                window.dispatchEvent(new KeyboardEvent('keydown', {
                  key: 'z',
                  ctrlKey: true,
                }));
              }

              // Assert.
              assertDeepEquals(
                  ['foo', false], handler.getArgs('setModuleDisabled')[1]);

              // Act.
              callbackRouterRemote.setDisabledModules(false, []);
              await callbackRouterRemote.$.flushForTesting();

              // Assert.
              assertNotStyle(moduleWrappers[0]!, 'display', 'none');
              assertFalse(modulesElement.$.undoToast.open);
              assertTrue(restoreCalled);
              assertEquals(
                  1, metrics.count('NewTabPage.Modules.Enabled', moduleId));
              assertEquals(
                  1,
                  metrics.count('NewTabPage.Modules.Enabled.Toast', moduleId));
            });

        test(
            `modules can be dismissed and restored via ${undoStrategy}`,
            async () => {
              const moduleId = 'foo';
              const fooDescriptor =
                  new ModuleDescriptor(moduleId, initNullModule);
              handler.setResultFor('getModulesIdNames', {
                data: [
                  {id: fooDescriptor.id, name: fooDescriptor.id},
                ],
              });
              const modulesElement = await createModulesElement(
                  [{
                    descriptor: fooDescriptor,
                    elements: [createElement()],
                  }],
                  true, SAMPLE_SCREEN_WIDTH);

              let moduleWrappers = modulesElement.shadowRoot!.querySelectorAll(
                  'ntp-module-wrapper');
              assertEquals(1, moduleWrappers.length);
              assertFalse(modulesElement.$.undoToast.open);

              let restoreCalled = false;
              moduleWrappers[0]!.dispatchEvent(
                  new CustomEvent('dismiss-module-instance', {
                    bubbles: true,
                    composed: true,
                    detail: {
                      message: 'Foo',
                      restoreCallback: () => {
                        restoreCalled = true;
                      },
                    },
                  }));

              assertEquals(
                  0,
                  modulesElement.shadowRoot!
                      .querySelectorAll('ntp-module-wrapper')
                      .length);
              assertTrue(modulesElement.$.undoToast.open);
              assertFalse(restoreCalled);
              assertEquals(1, handler.getCallCount('onDismissModule'));
              assertEquals(moduleId, handler.getArgs('onDismissModule')[0]);

              await waitAfterNextRender(modulesElement);
              if (undoStrategy === UndoStrategy.BUTTON_ACTIVATION) {
                const undoButton =
                    modulesElement.shadowRoot!.querySelector<HTMLElement>(
                        '#undoButton');
                assertTrue(!!undoButton);
                undoButton.click();
              } else if (undoStrategy === UndoStrategy.SHORTCUT_KEY) {
                window.dispatchEvent(new KeyboardEvent('keydown', {
                  key: 'z',
                  ctrlKey: true,
                }));
              }

              moduleWrappers = modulesElement.shadowRoot!.querySelectorAll(
                  'ntp-module-wrapper');
              assertEquals(1, moduleWrappers.length);
              assertFalse(modulesElement.$.undoToast.open);
              assertTrue(restoreCalled);
              assertEquals(
                  1, metrics.count('NewTabPage.Modules.Restored'),
                  'Restore metric value');
              assertEquals(1, metrics.count('NewTabPage.Modules.Restored.foo'));
            });
      });

  test('Undo shortcut ignored if no undo state', async () => {
    // Arrange
    const fooDescriptor = new ModuleDescriptor('foo', initNullModule);
    handler.setResultFor('getModulesIdNames', {
      data: [
        {id: fooDescriptor.id, name: fooDescriptor.id},
      ],
    });
    const modulesElement = await createModulesElement(
        [{
          descriptor: fooDescriptor,
          elements: [createElement()],
        }],
        true, SAMPLE_SCREEN_WIDTH);
    await waitAfterNextRender(modulesElement);

    // Act.
    window.dispatchEvent(new KeyboardEvent('keydown', {
      key: 'z',
      ctrlKey: true,
    }));

    // Assert: no crash.
  });

  function assertContainerLayout(
      moduleWrappers: ModuleWrapperElement[], scenario: Scenario) {
    assertEquals(scenario.count, moduleWrappers.length);

    let index = 0;
    scenario.rows.forEach((expectedRowWidths, i) => {
      expectedRowWidths.forEach((expectedWidth, j) => {
        const wrapper = moduleWrappers[index]!;
        const instance = wrapper.$.moduleElement.lastChild! as HTMLElement;
        assertEquals(expectedWidth.name, instance.getAttribute('format'));
        assertEquals(
            expectedWidth.value, wrapper.clientWidth,
            `Element at row ${i} column ${j}`);
        index++;
      });
    });

    assertEquals(
        scenario.rows.length,
        new Set(moduleWrappers.map(wrapper => wrapper.offsetTop)).size);
  }

  [{
    setup: [
      {name: 'foo', count: 3},
      {name: 'bar', count: 2},
      {name: 'baz', count: 1},
    ],
    before: {
      width: 1080,
      count: 6,
      rows: [
        [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
        [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
      ],
    },
    after: {
      width: 1080,
      count: 3,
      rows: [
        [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
      ],
    },
  },
   {
     setup: [
       {name: 'foo', count: 1},
       {name: 'bar', count: 2},
       {name: 'baz', count: 3},
     ],
     before: {
       width: 1080,
       count: 6,
       rows: [
         [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
         [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
       ],
     },
     after: {
       width: 1080,
       count: 5,
       rows: [
         [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
         [MEDIUM_WIDTH, MEDIUM_WIDTH],
       ],
     },
   }].forEach((layoutChangeScenario: LayoutChangeScenario, index) => {
    test(
        `Disabling one of many modules updates layout correctly ${index}`,
        async () => {
          const modules = layoutChangeScenario.setup.map(details => {
            return {
              descriptor: new ModuleDescriptor(details.name, initNullModule),
              elements: Array(details.count).fill(0).map(() => createElement()),
            };
          });
          handler.setResultFor('getModulesIdNames', {
            data: modules.map((module) => {
              return {id: module.descriptor.id, name: module.descriptor.id};
            }),
          });
          const modulesElement =
              await createModulesElement(modules, true, SAMPLE_SCREEN_WIDTH);
          await waitAfterNextRender(modulesElement);

          const moduleWrappers =
              Array.from(
                  modulesElement.shadowRoot!.querySelectorAll<HTMLElement>(
                      'ntp-module-wrapper')) as ModuleWrapperElement[];
          assertContainerLayout(moduleWrappers, layoutChangeScenario.before);

          moduleWrappers[0]!.dispatchEvent(new CustomEvent('disable-module', {
            bubbles: true,
            composed: true,
            detail: {
              message: 'Foo',
            },
          }));
          assertDeepEquals(
              ['foo', true], handler.getArgs('setModuleDisabled')[0]);
          callbackRouterRemote.setDisabledModules(false, ['foo']);
          await callbackRouterRemote.$.flushForTesting();
          await waitAfterNextRender(modulesElement);

          assertContainerLayout(
              Array.from(
                  modulesElement.shadowRoot!.querySelectorAll<HTMLElement>(
                      'ntp-module-wrapper:not([hidden])')) as
                  ModuleWrapperElement[],
              layoutChangeScenario.after);
        });
  });

  [{
    setup: [
      {name: 'foo', count: 3},
      {name: 'bar', count: 2},
      {name: 'baz', count: 1},
    ],
    before: {
      width: 1080,
      count: 6,
      rows: [
        [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
        [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
      ],
    },
    after: {
      width: 1080,
      count: 5,
      rows: [
        [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
        [MEDIUM_WIDTH, MEDIUM_WIDTH],
      ],
    },
  },
   {
     setup: [
       {name: 'foo', count: 1},
       {name: 'bar', count: 2},
       {name: 'baz', count: 3},
     ],
     before: {
       width: 1080,
       count: 6,
       rows: [
         [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
         [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
       ],
     },
     after: {
       width: 1080,
       count: 5,
       rows: [
         [NARROW_WIDTH, NARROW_WIDTH, NARROW_WIDTH],
         [MEDIUM_WIDTH, MEDIUM_WIDTH],
       ],
     },
   }].forEach((layoutChangeScenario: LayoutChangeScenario, index) => {
    test(
        `Dismissing one of many modules updates layout correctly ${index}`,
        async () => {
          const modules = layoutChangeScenario.setup.map(details => {
            return {
              descriptor: new ModuleDescriptor(details.name, initNullModule),
              elements: Array(details.count).fill(0).map(() => createElement()),
            };
          });
          handler.setResultFor('getModulesIdNames', {
            data: modules.map((module) => {
              return {id: module.descriptor.id, name: module.descriptor.id};
            }),
          });
          const modulesElement =
              await createModulesElement(modules, true, SAMPLE_SCREEN_WIDTH);
          await waitAfterNextRender(modulesElement);

          const moduleWrappers =
              Array.from(
                  modulesElement.shadowRoot!.querySelectorAll<HTMLElement>(
                      'ntp-module-wrapper')) as ModuleWrapperElement[];
          assertContainerLayout(moduleWrappers, layoutChangeScenario.before);

          let restoreCalled = false;
          moduleWrappers[0]!.dispatchEvent(
              new CustomEvent('dismiss-module-instance', {
                bubbles: true,
                composed: true,
                detail: {
                  message: 'Foo',
                  restoreCallback: () => {
                    restoreCalled = true;
                  },
                },
              }));
          assertFalse(restoreCalled);
          await waitAfterNextRender(modulesElement);

          assertContainerLayout(
              Array.from(
                  modulesElement.shadowRoot!.querySelectorAll<HTMLElement>(
                      'ntp-module-wrapper')) as ModuleWrapperElement[],
              layoutChangeScenario.after);
        });
  });
});