chromium/chrome/test/data/webui/history/test_util.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, ForeignSessionTab, ForeignSessionWindow, HistoryAppElement, HistoryEntry, HistoryQuery} from 'chrome://history/history.js';
import type {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {middleOfNode} from 'chrome://webui-test/mouse_mock_interactions.js';


/**
 * Create a fake history result with the given timestamp.
 * @param timestamp Timestamp of the entry, as a number in ms or a string which
 *     can be parsed by Date.parse().
 * @param urlStr The URL to set on this entry.
 */
export function createHistoryEntry(
    timestamp: number|string, urlStr: string): HistoryEntry {
  if (typeof timestamp === 'string') {
    timestamp += ' UTC';
  }

  const d = new Date(timestamp);
  const url = new URL(urlStr);
  const domain = url.host;
  return {
    allTimestamps: [d.getTime()],
    remoteIconUrlForUma: '',
    isUrlInRemoteUserData: false,
    blockedVisit: false,

    // Formatting the relative day is too hard, will instead display
    // YYYY-MM-DD.
    dateRelativeDay: d.toISOString().split('T')[0]!,
    dateShort: '',
    dateTimeOfDay: d.getUTCHours() + ':' + d.getUTCMinutes(),
    deviceName: '',
    deviceType: '',
    domain: domain,
    fallbackFaviconText: '',
    hostFilteringBehavior: 0,
    readableTimestamp: '',
    selected: false,
    snippet: '',
    starred: false,
    time: d.getTime(),
    title: urlStr,
    url: urlStr,
  };
}

/**
 * Create a fake history search result with the given timestamp. Replaces fields
 * from createHistoryEntry to look like a search result.
 * @param timestamp Timestamp of the entry, as a number in ms or a string which
 *     can be parsed by Date.parse().
 * @param urlStr The URL to set on this entry.
 */
export function createSearchEntry(
    timestamp: number|string, urlStr: string): HistoryEntry {
  const entry = createHistoryEntry(timestamp, urlStr);
  entry.dateShort = entry.dateRelativeDay;
  entry.dateTimeOfDay = '';
  entry.dateRelativeDay = '';

  return entry;
}

/**
 * Create a simple HistoryQuery.
 * @param searchTerm The search term that the info has. Will be empty  string if
 *     not specified.
 */
export function createHistoryInfo(searchTerm?: string): HistoryQuery {
  return {finished: true, term: searchTerm || ''};
}

export function polymerSelectAll(element: Element, selector: string): NodeList {
  return element.shadowRoot!.querySelectorAll(selector);
}

/**
 * Returns a promise which is resolved when |eventName| is fired on |element|
 * and |predicate| is true.
 */
export function waitForEvent(
    element: HTMLElement|Window, eventName: string,
    predicate?: (e: Event) => boolean): Promise<void> {
  if (!predicate) {
    predicate = () => true;
  }

  return new Promise<void>(function(resolve) {
    const listener = function(e: Event) {
      if (!predicate!(e)) {
        return;
      }

      resolve();
      element.removeEventListener(eventName, listener);
    };

    element.addEventListener(eventName, listener);
  });
}

/**
 * Sends a shift click event to |element|.
 */
export async function shiftClick(element: CrLitElement): Promise<void> {
  const xy = middleOfNode(element);
  const props = {
    bubbles: true,
    cancelable: true,
    clientX: xy.x,
    clientY: xy.y,
    buttons: 1,
    shiftKey: true,
  };
  element.dispatchEvent(new MouseEvent('mousedown', props));
  element.dispatchEvent(new MouseEvent('mouseup', props));
  element.dispatchEvent(new MouseEvent('click', props));
  await element.updateComplete;
}

/**
 * Sends a shift click event to |element|, using PointerEvent.
 */
export async function shiftPointerClick(element: CrLitElement): Promise<void> {
  const xy = middleOfNode(element);
  const props = {
    bubbles: true,
    cancelable: true,
    clientX: xy.x,
    clientY: xy.y,
    buttons: 1,
    shiftKey: true,
  };
  element.dispatchEvent(new PointerEvent('pointerdown', props));
  element.dispatchEvent(new PointerEvent('pointerup', props));
  element.dispatchEvent(new PointerEvent('click', props));
  await element.updateComplete;
}

export function disableLinkClicks() {
  document.addEventListener('click', function(e) {
    if (e.defaultPrevented) {
      return;
    }

    const eventPath = e.composedPath();
    let anchor = null;
    if (eventPath) {
      for (let i = 0; i < eventPath.length; i++) {
        const element = eventPath[i] as HTMLElement;
        if (element.tagName === 'A' && (element as HTMLAnchorElement).href) {
          anchor = element;
          break;
        }
      }
    }

    if (!anchor) {
      return;
    }

    e.preventDefault();
  });
}

export function createSession(
    name: string, windows: ForeignSessionWindow[]): ForeignSession {
  return {
    collapsed: false,
    name,
    modifiedTime: '2 seconds ago',
    tag: name,
    timestamp: 0,
    windows,
  };
}

export function createWindow(tabUrls: string[]): ForeignSessionWindow {
  const tabs: ForeignSessionTab[] = tabUrls.map(function(tabUrl) {
    return {
      direction: '',
      remoteIconUrlForUma: '',
      sessionId: 456,
      timestamp: 0,
      title: tabUrl,
      type: 'tab',
      url: tabUrl,
      windowId: 0,
    };
  });

  return {tabs: tabs, sessionId: 123, timestamp: 0};
}

export function navigateTo(route: string, _app: HistoryAppElement) {
  window.history.replaceState({}, '', route);
  window.dispatchEvent(new CustomEvent('popstate'));
  flush();
}