Home Reference Source

lib/clicks.js

import { delay, waitForPredicate } from './delays';
import { scrollIntoView } from './scrolls';
import { qs, qsa } from './dom';
import { createMouseEvent, fireMouseEventsOnElement } from './events';
import {
  browserLocation,
  waitForHistoryManipToChangeLocation,
} from './browser';

/**
 * @type {number}
 * @private
 */
let __currentClickCount = 0;

/**
 * @type {Array<string>}
 * @private
 */
const __clickPreEvents = ['mousemove', 'mouseover', 'mousedown', 'mouseup'];

/**
 * Calls the click function on the supplied element if non-null/defined.
 * Returns true or false to indicate if the click happened
 * @param {?SomeElement} elem - The element to be clicked
 * @return {boolean} - T/F to indicate that click happened.
 */
export function click(elem) {
  let clicked = false;
  if (elem != null) {
    fireMouseEventsOnElement({
      elem,
      eventNames: __clickPreEvents,
      clickCount: __currentClickCount,
    });
    __currentClickCount += 1;
    elem.click();
    elem.dispatchEvent(
      createMouseEvent({
        type: 'mouseleave',
        elem,
        clickCount: __currentClickCount,
      })
    );
    clicked = true;
  }
  return clicked;
}

/**
 * Calls the click function on the supplied element if non-null/defined
 * that exists in the JS context of the supplied window (cntx).
 * Returns true or false to indicate if the click happened
 * @param {SomeElement} elem - The element to be clicked
 * @param {Window} cntx - The context window
 * @return {boolean} - T/F to indicate that click happened.
 */
export function clickInContext(elem, cntx) {
  let clicked = false;
  if (elem != null) {
    fireMouseEventsOnElement({
      elem,
      eventNames: __clickPreEvents,
      view: cntx,
    });
    elem.click();
    elem.dispatchEvent(
      createMouseEvent({ type: 'mouseleave', view: cntx, elem })
    );
    clicked = true;
  }
  return clicked;
}

/**
 * Calls the click method of the supplied element and waits the
 * specified delayTime after clicking IFF the supplied element was
 * non-null/undefined.
 * @param {?SomeElement} elem - The element to be clicked
 * @param {number} [delayTime = 1000] - How long is the delay after click
 * @return {Promise<boolean>} - A Promise that resolves to T/F
 * indicating if the click happened
 */
export async function clickWithDelay(elem, delayTime) {
  let clicked = click(elem);
  if (clicked) {
    await delay(delayTime || 1000);
  }
  return clicked;
}

/**
 * Calls the click method of the supplied element that exists in
 * the JS context of the supplied window object (cntx) and waits the
 * specified delayTime after clicking IFF the supplied element was
 * non-null/undefined.
 * @param {HTMLElement|Element} elem - The element to be clicked
 * @param {Window} cntx - The context window
 * @param {number} [delayTime = 1000] - How long is the delay after click
 * @return {Promise<boolean>} - A Promise that resolves to T/F
 * indicating if the click happened
 */
export async function clickInContextWithDelay(elem, cntx, delayTime) {
  const clicked = clickInContext(elem, cntx);
  if (clicked) {
    await delay(delayTime || 1000);
  }
  return clicked;
}

/**
 * Scrolls the supplied element into view and clicks it if the
 * element is non-null/undefined
 * @param {HTMLElement|Element} elem - The element to be scrolled into view
 * and clicked.
 * @return {boolean} - T/F to indicate that the click happened
 */
export function scrollIntoViewAndClick(elem) {
  scrollIntoView(elem);
  return click(elem);
}

/**
 * Scrolls the supplied element into view and clicks it if the
 * element is non-null/undefined and waits for the specified delay time
 * @param {HTMLElement|Element} elem - The element to be
 * @param {number} [delayTime = 1000] - How long is the delay
 * @return {Promise<boolean>} - A Promise that resolves to T/F indicating
 * that the click happened
 */
export function scrollIntoViewAndClickWithDelay(elem, delayTime) {
  scrollIntoView(elem);
  return clickWithDelay(elem, delayTime || 1000);
}

/**
 * Scrolls into view and clicks all the elements
 * present in the supplied array.
 * @param {Array<HTMLElement>} elems - The array of elements
 * to be scrolled into view and clicked
 * @return {number} - The number of elements clicked out of the supplied
 * array of elements
 */
export function scrollAllIntoViewAndClick(elems) {
  let totalClicks = 0;
  for (var i = 0; i < elems.length; ++i) {
    scrollIntoView(elems[i]);
    if (click(elems[i])) totalClicks += 1;
  }
  return totalClicks;
}

/**
 * Scrolls into view and clicks all the elements
 * present in the supplied array waiting for the supplied
 * delay time after each click of an element in the array.
 * @param {Array<HTMLElement|Element|Node>} elems - The array of elements
 * to be scrolled into view and clicked
 * @param {number} [delayTime = 1000] - How long is the delay after click
 * @return {Promise<void>}
 */
export async function scrollAllIntoViewAndClickWithDelay(elems, delayTime) {
  const delayAmount = delayTime || 1000;
  for (var i = 0; i < elems.length; ++i) {
    scrollIntoView(elems[i]);
    await clickWithDelay(elems[i], delayAmount);
  }
}

/**
 * Clicks the element from executing document.querySelector using
 * the supplied selector
 * @param {string} selector - the css selector to use
 * @param {*} [cntx] - element to use rather than document for the querySelector call
 * @return {boolean} - T/F to indicate that the click happened
 */
export function selectElemAndClick(selector, cntx) {
  return click(qs(selector, cntx));
}

/**
 * Clicks the element from executing document.querySelector using
 * the supplied selector and waiting the specified delay time after clicking
 * @param {string} selector - the css selector to use
 * @param {number} [delayTime = 1000] - How long is the delay
 * @return {Promise<boolean>} - A Promise that resolves to T/F indicating
 * that the click happened
 */
export function selectElemAndClickWithDelay(selector, delayTime) {
  return clickWithDelay(document.querySelector(selector), delayTime || 1000);
}

/**
 * Clicks the element from executing querySelector using
 * the supplied selector using the supplied element
 * @param {Element|Node|HTMLElement} selectFrom - element to use for the querySelector call
 * @param {string} selector - the css selector to use
 * @return {boolean} - T/F to indicate that the click happened
 */
export function selectElemFromAndClick(selectFrom, selector) {
  return click(qs(selector, selectFrom));
}

/**
 * Clicks the element from executing querySelector using
 * the supplied selector from the supplied element and waiting the specified delay time after clicking
 * @param {Element|Node|HTMLElement} selectFrom - element to use for the querySelector call
 * @param {string} selector - the css selector to use
 * @param {number} [delayTime = 1000] - How long is the delay
 * @return {Promise<boolean>} - A Promise that resolves to T/F indicating
 * that the click happened
 */
export function selectElemFromAndClickWithDelay(
  selectFrom,
  selector,
  delayTime
) {
  return clickWithDelay(qs(selector, selectFrom), delayTime || 1000);
}

/**
 * Clicks the supplied element and then waits for the supplied predicate function to return true
 * @param {Element|Node|HTMLElement} elem - the element to be clicked
 * @param {function(): boolean} predicate - function returning true or false indicating the wait condition is satisfied
 * @param {?WaitForOptions} [options] - Options controlling how the wait
 * will happen
 * @return {Promise<{predicate: boolean, maxExceeded: boolean, clicked: boolean}>} - A Promise that resolves with
 * an object indicating the results of the operation
 */
export async function clickAndWaitFor(elem, predicate, options) {
  const results = { clicked: false, maxExceeded: false, predicate: false };
  results.clicked = click(elem);
  if (results.clicked) {
    const waitr = await waitForPredicate(predicate, options);
    results.predicate = waitr.predicate;
    results.maxExceeded = waitr.maxExceeded;
  }
  return results;
}

/**
 * Clicks the element returned by calling querySelector using the selector
 * on document or the supplied element (selectFrom) and then waits for the supplied
 * predicate function to return true
 * @param {string} selector - the css selector to use
 * @param {function(): boolean} predicate - function returning true or false indicating the wait condition is satisfied
 * @param {HTMLElement|Document} [selectFrom] - optional element to use for the querySelector(selector) call
 * @return {Promise<{predicate: boolean, maxExceeded: boolean, clicked: boolean}>} - A Promise that resolves with
 * an object indicating the results of the operation
 */
export function selectClickAndWaitFor(selector, predicate, selectFrom) {
  return clickAndWaitFor(qs(selector, selectFrom), predicate);
}

/**
 * Clicks the element returned from result of calling querySelector using the supplied
 * selector on the supplied element N number of times. Note that the evaluation of the querySelector
 * is done all N times.
 * @param {Element} selectFrom - element to use for the querySelector(selector) call
 * @param {string} selector - the css selector to use
 * @param {number} n - How many times to click the element returned by
 * performing selectFrom.querySelector(selector)
 * @return {number} - How many clicks occurred
 */
export function selectFromAndClickNTimes(selectFrom, selector, n) {
  let totalClicks = 0;
  let clickMe;
  for (var i = 0; i < n; ++i) {
    clickMe = qs(selector, selectFrom);
    if (click(clickMe)) {
      totalClicks += 1;
    }
  }
  return totalClicks;
}

/**
 * Clicks the element returned from result of calling querySelector using the supplied
 * selector on the supplied element N number of times and waits for delay time afters each click.
 * Note that the evaluation of the querySelector is done all N times.
 * @param {Element} selectFrom - The element to use to call querySelector(selector) on
 * @param {string} selector - the css selector to use
 * @param {number} n - How many times to click the element returned by
 * performing selectFrom.querySelector(selector)
 * @param {number} [delayTime = 1000] - How long is the delay
 * @return {Promise<number>} - How many clicks occurred
 */
export async function selectFromAndClickNTimesWithDelay(
  selectFrom,
  selector,
  n,
  delayTime
) {
  let totalClicks = 0;
  const delayAmount = delayTime || 1000;
  let clickMe;
  for (var i = 0; i < n; ++i) {
    clickMe = qs(selector, selectFrom);
    if (await clickWithDelay(clickMe, delayAmount)) {
      totalClicks += 1;
    }
  }
  return totalClicks;
}

/**
 * Clicks the element returned from result of calling querySelector using the supplied
 * selector on the supplied element until the results of the evaluting the selector returns null/undefined,
 * waiting for delay time afters each click.
 * @param {Element} selectFrom - The element to use to call querySelector(selector) on
 * @param {string} selector - the css selector to use when performing selectFrom.querySelector(selector)
 * @param {{delay: number, max: ?number}} [options] - options about delayTime and safety time
 * @return {Promise<number>} - How many clicks occurred
 */
export async function selectFromAndClickUntilNullWithDelay(
  selectFrom,
  selector,
  options
) {
  let totalClicks = 0;
  const opts = Object.assign({ delay: 1000 }, options);
  let exit = false;
  let safety;
  if (opts.max) {
    safety = setTimeout(() => {
      exit = true;
    }, opts.max);
  }
  let clickMe = qs(selector, selectFrom);
  while (clickMe != null) {
    if (!clickMe.isConnected) break;
    if (exit) break;
    await clickWithDelay(clickMe, opts.delay);
    clickMe = qs(selector, selectFrom);
    totalClicks += 1;
  }
  if (opts.safety) {
    clearTimeout(safety);
  }
  return totalClicks;
}

/**
 * Like {@link selectFromAndClickUntilNullWithDelay} but will stop
 * selecting and clicking the selected element if the current reference
 * becomes disconnected from the dom or the optional stopPredicate
 * returns true. There is no dealy for delay you want
 * {@link selectScrollIntoViewAndClickWithDelayWhileSelectedConnected}
 * @param {string} selector - The selector to be used to select the element
 * @param {SelectScrollIntoViewClickUntilNullOptions} options
 * @return {void}
 */
export function selectScrollIntoViewAndClickSelectedWhileConnected(
  selector,
  options = {}
) {
  let selected = qs(selector, options.cntx);
  const haveStopPred = typeof options.stopPredicate === 'function';
  while (selected) {
    if (!selected.isConnected) break;
    scrollIntoViewAndClick(selected);
    if (haveStopPred && options.stopPredicate(selected)) break;
    selected = qs(selector, options.cntx);
  }
}

/**
 * Like {@link selectFromAndClickUntilNullWithDelay} but will stop
 * selecting and clicking the selected element if the current reference
 * becomes disconnected from the dom or the optional stopPredicate
 * returns true.
 * @param {string} selector - The selector to be used to select the element
 * @param {SelectScrollIntoViewClickUntilNullOptions} [options] - Additional configuration options
 * @return {Promise<void>}
 */
export async function selectScrollIntoViewAndClickWithDelayWhileSelectedConnected(
  selector,
  options = {}
) {
  let selected = qs(selector, options.cntx);
  const haveStopPred = typeof options.stopPredicate === 'function';
  while (selected) {
    if (!selected.isConnected) break;
    await scrollIntoViewAndClickWithDelay(selected, options.delay);
    if (haveStopPred && options.stopPredicate(selected)) break;
    selected = qs(selector, options.cntx);
  }
}

/**
 * Selects all elements that match the supplied selector and clicks them
 * returning T/F indicating if a click happened on any selected element
 * @param {string} selector
 * @param {Element} [context]
 * @return {boolean}
 */
export function selectAllAndClick(selector, context) {
  const selected = qsa(selector, context);
  let anyClicked = false;
  for (let i = 0; i < selected.length; i++) {
    if (click(selected[i])) anyClicked = true;
  }
  return anyClicked;
}

/**
 * Selects all elements that match the supplied selector and clicks them
 * returning T/F indicating if a click happened on any selected element
 * @param {{selector: string, cntx: *, delayTime: *}} opts
 * @return {boolean}
 */
export async function selectAllAndClickWithDelay({ selector, cntx, delay }) {
  const selected = qsa(selector, cntx);
  let anyClicked = false;
  for (let i = 0; i < selected.length; i++) {
    let wasClicked = await clickWithDelay(selected[i], delay);
    anyClicked = wasClicked || anyClicked;
  }
  return anyClicked;
}

/**
 * Clicks the supplied element and waits for browser history to change (the URL is updated by JavaScript modifying the current history stack)
 * @param {Element|HTMLElement} elemToBeClicked - the Element to be click
 * @param {WaitForOptions} [waitOpts] - options controlling the wait
 * @return {Promise<{oldLocation: ?string, historyChanged: boolean, clicked: boolean, newLocation: ?string, ok: boolean}>}
 */
export async function clickAndWaitForHistoryChange(elemToBeClicked, waitOpts) {
  const results = {
    clicked: false,
    historyChanged: false,
    ok: false,
    oldLocation: browserLocation(),
    newLocation: null,
  };
  results.clicked = click(elemToBeClicked);
  if (results.clicked) {
    results.historyChanged = await waitForHistoryManipToChangeLocation(
      results.oldLocation,
      waitOpts
    );
  }
  if (results.historyChanged) {
    results.newLocation = browserLocation();
  }
  results.ok = results.clicked && results.historyChanged;
  return results;
}

/**
 * @typedef {Object} SelectScrollIntoViewClickUntilNullOptions
 * @property {Document|Element|HTMLElement} [cntx] - optional element to call querySelector on using selector
 * @property {function(selected: Element): boolean} [stopPredicate] - optional function called to determine if the loop should end after each iteration
 * @property{number} [delay] - amount of time, in milliseconds, that should be waited after scrolling into view and clicking (defaults to 1 second) -- Only used if the function will apply a delay
 */