chromium/chrome/test/data/webui/history/history_synced_tabs_test.ts

// Copyright 2016 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 {ForeignSession, HistorySyncedDeviceCardElement, HistorySyncedDeviceManagerElement} from 'chrome://history/history.js';
import {BrowserServiceImpl, ensureLazyLoaded} from 'chrome://history/history.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks, waitBeforeNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {microtasksFinished} from 'chrome://webui-test/test_util.js';

import {TestBrowserService} from './test_browser_service.js';
import {createSession, createWindow} from './test_util.js';

function getCards(manager: HistorySyncedDeviceManagerElement):
    NodeListOf<HistorySyncedDeviceCardElement> {
  return manager.shadowRoot!.querySelectorAll('history-synced-device-card');
}

function numWindowSeparators(card: HistorySyncedDeviceCardElement): number {
  return card.shadowRoot!.querySelectorAll(':not([hidden]).window-separator')
      .length;
}

function assertNoSyncedTabsMessageShown(
    manager: HistorySyncedDeviceManagerElement, stringID: string) {
  assertFalse(manager.$['no-synced-tabs'].hidden);
  const message = loadTimeData.getString(stringID);
  assertNotEquals(
      -1, manager.$['no-synced-tabs'].textContent!.indexOf(message));
}

suite('<history-synced-device-manager>', function() {
  let element: HistorySyncedDeviceManagerElement;
  let testService: TestBrowserService;

  function setForeignSessions(sessions: ForeignSession[]) {
    element.sessionList = sessions;
  }

  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    window.history.replaceState({}, '', '/');
    testService = new TestBrowserService();
    BrowserServiceImpl.setInstance(testService);

    // Need to ensure lazy_load.html has been imported so that the device
    // manager custom element is defined.
    return ensureLazyLoaded().then(() => {
      element = document.createElement('history-synced-device-manager');
      // |signInState| is generally set after |searchTerm| in Polymer 2. Set in
      // the same order in tests, in order to catch regressions like
      // https://crbug.com/915641.
      element.searchTerm = '';
      element.configureSignInForTest(
          {signInState: true, signInAllowed: true, guestSession: false});
      document.body.appendChild(element);
    });
  });

  test('single card, single window', function() {
    const sessionList: ForeignSession[] = [createSession(
        'Nexus 5',
        [createWindow(['http://www.google.com', 'http://example.com'])])];
    setForeignSessions(sessionList);

    return flushTasks().then(function() {
      const card =
          element.shadowRoot!.querySelector('history-synced-device-card')!;
      assertEquals(
          'http://www.google.com',
          card.shadowRoot!.querySelectorAll<HTMLElement>(
                              '.website-title')[0]!.textContent!.trim());
      assertEquals(2, card.tabs.length);
    });
  });

  test('two cards, multiple windows', function() {
    const sessionList: ForeignSession[] = [
      createSession(
          'Nexus 5',
          [createWindow(['http://www.google.com', 'http://example.com'])]),
      createSession(
          'Nexus 6',
          [
            createWindow(['http://test.com']),
            createWindow(['http://www.gmail.com', 'http://badssl.com']),
          ]),
    ];
    setForeignSessions(sessionList);

    return flushTasks().then(function() {
      const cards = getCards(element);
      assertEquals(2, cards.length);

      // Ensure separators between windows are added appropriately.
      assertEquals(0, numWindowSeparators(cards[0]!));
      assertEquals(1, numWindowSeparators(cards[1]!));
    });
  });

  test('updating sessions', function() {
    const session1 = createSession(
        'Chromebook',
        [createWindow(['http://www.example.com', 'http://crbug.com'])]);
    session1.timestamp = 1000;

    const session2 =
        createSession('Nexus 5', [createWindow(['http://www.google.com'])]);

    setForeignSessions([session1, session2]);

    return flushTasks()
        .then(function() {
          const session1updated = createSession('Chromebook', [
            createWindow(['http://www.example.com', 'http://crbug.com/new']),
            createWindow(['http://web.site']),
          ]);
          session1updated.timestamp = 1234;

          setForeignSessions([session1updated, session2]);

          return flushTasks();
        })
        .then(function() {
          // There should only be two cards.
          const cards = getCards(element);
          assertEquals(2, cards.length);

          // There are now 2 windows in the first device.
          assertEquals(1, numWindowSeparators(cards[0]!));

          // Check that the actual link changes.
          assertEquals(
              'http://crbug.com/new',
              cards[0]!.shadowRoot!
                  .querySelectorAll<HTMLElement>(
                      '.website-title')[1]!.textContent!.trim());
        });
  });

  test('two cards, multiple windows, search', function() {
    const sessionList: ForeignSession[] = [
      createSession(
          'Nexus 5',
          [createWindow(['http://www.google.com', 'http://example.com'])]),
      createSession(
          'Nexus 6',
          [
            createWindow(['http://www.gmail.com', 'http://badssl.com']),
            createWindow(['http://test.com']),
            createWindow(['http://www.gmail.com', 'http://bagssl.com']),
          ]),
    ];
    setForeignSessions(sessionList);

    return flushTasks()
        .then(function() {
          const cards = getCards(element);
          assertEquals(2, cards.length);

          // Ensure separators between windows are added appropriately.
          assertEquals(0, numWindowSeparators(cards[0]!));
          assertEquals(2, numWindowSeparators(cards[1]!));
          element.searchTerm = 'g';

          return flushTasks();
        })
        .then(function() {
          const cards = getCards(element);

          assertEquals(0, numWindowSeparators(cards[0]!));
          assertEquals(1, cards[0]!.tabs.length);
          assertEquals('http://www.google.com', cards[0]!.tabs[0]!.title);
          assertEquals(1, numWindowSeparators(cards[1]!));
          assertEquals(3, cards[1]!.tabs.length);
          assertEquals('http://www.gmail.com', cards[1]!.tabs[0]!.title);
          assertEquals('http://www.gmail.com', cards[1]!.tabs[1]!.title);
          assertEquals('http://bagssl.com', cards[1]!.tabs[2]!.title);

          // Ensure the title text is rendered during searches.
          assertEquals(
              'http://www.google.com',
              cards[0]!.shadowRoot!
                  .querySelectorAll<HTMLElement>(
                      '.website-title')[0]!.textContent!.trim());

          element.searchTerm = 'Sans';
          return flushTasks();
        })
        .then(function() {
          assertEquals(0, getCards(element).length);

          assertNoSyncedTabsMessageShown(element, 'noSearchResults');
        });
  });

  test('delete a session', function() {
    const sessionList: ForeignSession[] = [
      createSession('Nexus 5', [createWindow(['http://www.example.com'])]),
      createSession('Pixel C', [createWindow(['http://www.badssl.com'])]),
    ];

    setForeignSessions(sessionList);

    return flushTasks()
        .then(function() {
          const cards = getCards(element);
          assertEquals(2, cards.length);

          cards[0]!.$['menu-button'].click();
          return flushTasks();
        })
        .then(function() {
          element.shadowRoot!.querySelector<HTMLElement>(
                                 '#menuDeleteButton')!.click();
          return testService.whenCalled('deleteForeignSession');
        })
        .then(args => {
          assertEquals('Nexus 5', args);

          // Simulate deleting the first device.
          setForeignSessions([sessionList[1]!]);

          return flushTasks();
        })
        .then(function() {
          const cards = getCards(element);
          assertEquals(1, cards.length);
          assertEquals('http://www.badssl.com', cards[0]!.tabs[0]!.title);
        });
  });

  test('delete a collapsed session', async () => {
    const sessionList: ForeignSession[] = [
      createSession('Nexus 5', [createWindow(['http://www.example.com'])]),
      createSession('Pixel C', [createWindow(['http://www.badssl.com'])]),
    ];

    setForeignSessions(sessionList);
    await flushTasks();

    let cards = getCards(element);
    cards[0]!.$['card-heading'].click();
    await microtasksFinished();
    assertFalse(cards[0]!.opened);

    // Simulate deleting the first device.
    setForeignSessions([sessionList[1]!]);
    await flushTasks();
    cards = getCards(element);
    assertTrue(cards[0]!.opened);
  });

  test('click synced tab', function() {
    setForeignSessions(
        [createSession('Chromebook', [createWindow(['https://example.com'])])]);
    return flushTasks()
        .then(function() {
          const cards = getCards(element);
          const anchor = cards[0]!.shadowRoot!.querySelector('a')!;
          anchor.click();
          return testService.whenCalled('openForeignSessionTab');
        })
        .then(args => {
          assertEquals('Chromebook', args.sessionTag, 'sessionTag is correct');
          assertEquals(456, args.tabId, 'tabId is correct');
          assertFalse(args.e.altKey, 'altKey is defined');
          assertFalse(args.e.ctrlKey, 'ctrlKey is defined');
          assertFalse(args.e.metaKey, 'metaKey is defined');
          assertFalse(args.e.shiftKey, 'shiftKey is defined');
        });
  });

  test('show actions menu', function() {
    setForeignSessions(
        [createSession('Chromebook', [createWindow(['https://example.com'])])]);

    return flushTasks().then(function() {
      const cards = getCards(element);
      cards[0]!.$['menu-button'].click();
      assertTrue(element.$.menu.getIfExists()!.open);
    });
  });

  test('show sign in promo', function() {
    element.configureSignInForTest(
        {signInState: false, signInAllowed: true, guestSession: false});
    return flushTasks()
        .then(function() {
          assertFalse(element.$['sign-in-guide'].hidden);
          element.configureSignInForTest(
              {signInState: true, signInAllowed: true, guestSession: false});
          return flushTasks();
        })
        .then(function() {
          assertTrue(element.$['sign-in-guide'].hidden);
        });
  });

  test('no synced tabs message', function() {
    // When user is not logged in, there is no synced tabs.
    element.configureSignInForTest(
        {signInState: false, signInAllowed: true, guestSession: false});
    element.clearSyncedDevicesForTest();
    return flushTasks()
        .then(function() {
          assertTrue(element.$['no-synced-tabs'].hidden);

          const cards = getCards(element);
          assertEquals(0, cards.length);

          element.configureSignInForTest(
              {signInState: true, signInAllowed: true, guestSession: false});

          return flushTasks();
        })
        .then(function() {
          // When user signs in, first show loading message.
          assertNoSyncedTabsMessageShown(element, 'loading');

          const sessionList: ForeignSession[] = [];
          setForeignSessions(sessionList);
          return flushTasks();
        })
        .then(function() {
          const cards = getCards(element);
          assertEquals(0, cards.length);
          // If no synced tabs are fetched, show 'no synced tabs'.
          assertNoSyncedTabsMessageShown(element, 'noSyncedResults');

          const sessionList: ForeignSession[] = [createSession(
              'Nexus 5',
              [createWindow(['http://www.google.com', 'http://example.com'])])];
          setForeignSessions(sessionList);

          return flushTasks();
        })
        .then(function() {
          const cards = getCards(element);
          assertEquals(1, cards.length);
          // If there are any synced tabs, hide the 'no synced tabs' message.
          assertTrue(element.$['no-synced-tabs'].hidden);

          element.configureSignInForTest(
              {signInState: false, signInAllowed: true, guestSession: false});
          return flushTasks();
        })
        .then(function() {
          // When user signs out, don't show the message.
          assertTrue(element.$['no-synced-tabs'].hidden);
        });
  });

  test('hide sign in promo in guest mode', function() {
    element.configureSignInForTest(
        {signInState: false, signInAllowed: true, guestSession: true});
    return flushTasks().then(function() {
      assertTrue(element.$['sign-in-guide'].hidden);
    });
  });

  test('hide sign-in promo if sign-in is disabled', async function() {
    element.configureSignInForTest(
        {signInState: false, signInAllowed: false, guestSession: false});
    await flushTasks();
    assertTrue(element.$['sign-in-guide'].hidden);
  });

  test('no synced tabs message displays on load', function() {
    element.clearSyncedDevicesForTest();
    // Should show no synced tabs message on initial load. Regression test for
    // https://crbug.com/915641.
    return Promise.all([flushTasks(), waitBeforeNextRender(element)])
        .then(() => {
          assertNoSyncedTabsMessageShown(element, 'noSyncedResults');
          const cards = getCards(element);
          assertEquals(0, cards.length);
        });
  });
});