Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/devtools/client/debugger/test/mochitest/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 103 kB image not shown  

Quellcode-Bibliothek shared-head.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */


// This file is loaded in a `spawn` context sometimes which doesn't have,
// `Assert`, so we can't use its comparison functions.
/* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */

/**
 * Helper methods to drive with the debugger during mochitests. This file can be safely
 * required from other panel test files.
 */


"use strict";

/* eslint-disable no-unused-vars */

// We can't use "import globals from head.js" because of bug 1395426.
// So workaround by manually importing the few symbols we are using from it.
// (Note that only ./mach eslint devtools/client fails while devtools/client/debugger passes)
/* global EXAMPLE_URL, ContentTask */

// Assume that shared-head is always imported before this file
/* import-globals-from ../../../shared/test/shared-head.js */

/**
 * Helper method to create a "dbg" context for other tools to use
 */

function createDebuggerContext(toolbox) {
  const panel = toolbox.getPanel("jsdebugger");
  const win = panel.panelWin;

  return {
    ...win.dbg,
    commands: toolbox.commands,
    toolbox,
    win,
    panel,
  };
}

var { Toolbox } = require("devtools/client/framework/toolbox");
const asyncStorage = require("devtools/shared/async-storage");

const {
  getSelectedLocation,
} = require("devtools/client/debugger/src/utils/selected-location");
const {
  createLocation,
} = require("devtools/client/debugger/src/utils/location");

const {
  resetSchemaVersion,
} = require("devtools/client/debugger/src/utils/prefs");

const {
  getUnicodeUrlPath,
} = require("resource://devtools/client/shared/unicode-url.js");

const {
  isGeneratedId,
} = require("devtools/client/shared/source-map-loader/index");

const DEBUGGER_L10N = new LocalizationHelper(
  "devtools/client/locales/debugger.properties"
);

const isCm6Enabled = Services.prefs.getBoolPref(
  "devtools.debugger.features.codemirror-next"
);

/**
 * Waits for `predicate()` to be true. `state` is the redux app state.
 *
 * @param {Object} dbg
 * @param {Function} predicate
 * @param {String} msg
 * @return {Promise}
 */

function waitForState(dbg, predicate, msg = "") {
  return new Promise(resolve => {
    info(`Waiting for state change: ${msg}`);
    let result = predicate(dbg.store.getState());
    if (result) {
      info(
        `--> The state was immediately correct (should rather do an immediate assertion?)`
      );
      resolve(result);
      return;
    }

    const unsubscribe = dbg.store.subscribe(() => {
      result = predicate(dbg.store.getState());
      if (result) {
        info(`Finished waiting for state change: ${msg}`);
        unsubscribe();
        resolve(result);
      }
    });
  });
}

/**
 * Waits for sources to be loaded.
 *
 * @memberof mochitest/waits
 * @param {Object} dbg
 * @param {Array} sources
 * @return {Promise}
 * @static
 */

async function waitForSources(dbg, ...sources) {
  if (sources.length === 0) {
    return;
  }

  info(`Waiting on sources: ${sources.join(", ")}`);
  await Promise.all(
    sources.map(url => {
      if (!sourceExists(dbg, url)) {
        return waitForState(
          dbg,
          () => sourceExists(dbg, url),
          `source ${url} exists`
        );
      }
      return Promise.resolve();
    })
  );

  info(`Finished waiting on sources: ${sources.join(", ")}`);
}

/**
 * Waits for a source to be loaded.
 *
 * @memberof mochitest/waits
 * @param {Object} dbg
 * @param {String} source
 * @return {Promise}
 * @static
 */

function waitForSource(dbg, url) {
  return waitForState(
    dbg,
    () => findSource(dbg, url, { silent: true }),
    "source exists"
  );
}

async function waitForElement(dbg, name, ...args) {
  await waitUntil(() => findElement(dbg, name, ...args));
  return findElement(dbg, name, ...args);
}

/**
 * Wait for a count of given elements to be rendered on screen.
 *
 * @param {DebuggerPanel} dbg
 * @param {String} name
 * @param {Integer} count: Number of elements to match. Defaults to 1.
 * @param {Boolean} countStrictlyEqual: When set to true, will wait until the exact number
 *                  of elements is displayed on screen. When undefined or false, will wait
 *                  until there's at least `${count}` elements on screen (e.g. if count
 *                  is 1, it will resolve if there are 2 elements rendered).
 */

async function waitForAllElements(
  dbg,
  name,
  count = 1,
  countStrictlyEqual = false
) {
  await waitUntil(() => {
    const elsCount = findAllElements(dbg, name).length;
    return countStrictlyEqual ? elsCount === count : elsCount >= count;
  });
  return findAllElements(dbg, name);
}

async function waitForElementWithSelector(dbg, selector) {
  await waitUntil(() => findElementWithSelector(dbg, selector));
  return findElementWithSelector(dbg, selector);
}

function waitForRequestsToSettle(dbg) {
  return dbg.commands.client.waitForRequestsToSettle();
}

function assertClass(el, className, exists = true) {
  if (exists) {
    ok(el.classList.contains(className), `${className} class exists`);
  } else {
    ok(!el.classList.contains(className), `${className} class does not exist`);
  }
}

function waitForSelectedLocation(dbg, line, column) {
  return waitForState(dbg, () => {
    const location = dbg.selectors.getSelectedLocation();
    return (
      location &&
      (line ? location.line == line : true) &&
      (column ? location.column == column : true)
    );
  });
}

/**
 * Wait for a given source to be selected and ready.
 *
 * @memberof mochitest/waits
 * @param {Object} dbg
 * @param {null|string|Source} sourceOrUrl Optional. Either a source URL (string) or a source object (typically fetched via `findSource`)
 * @return {Promise}
 * @static
 */

function waitForSelectedSource(dbg, sourceOrUrl) {
  const {
    getSelectedSourceTextContent,
    getSymbols,
    getBreakableLines,
    getSourceActorsForSource,
    getSourceActorBreakableLines,
    getFirstSourceActorForGeneratedSource,
    getSelectedFrame,
    getCurrentThread,
  } = dbg.selectors;

  return waitForState(
    dbg,
    () => {
      const location = dbg.selectors.getSelectedLocation() || {};
      const sourceTextContent = getSelectedSourceTextContent();
      if (!sourceTextContent) {
        return false;
      }

      if (sourceOrUrl) {
        // Second argument is either a source URL (string)
        // or a Source object.
        if (typeof sourceOrUrl == "string") {
          const url = location.source.url;
          if (typeof url != "string" || !url.includes(encodeURI(sourceOrUrl))) {
            return false;
          }
        } else if (location.source.id != sourceOrUrl.id) {
          return false;
        }
      }

      // Only when we are paused on that specific source (and this isn't a WASM source, which has no AST):
      // wait for symbols/AST to be parsed
      if (
        getSelectedFrame(getCurrentThread())?.location.source.id ==
          location.source.id &&
        !getSymbols(location) &&
        !isWasmBinarySource(location.source)
      ) {
        return false;
      }

      // Finaly wait for breakable lines to be set
      if (location.source.isHTML) {
        // For HTML sources we need to wait for each source actor to be processed.
        // getBreakableLines will return the aggregation without being able to know
        // if that's complete, with all the source actors.
        const sourceActors = getSourceActorsForSource(location.source.id);
        const allSourceActorsProcessed = sourceActors.every(
          sourceActor => !!getSourceActorBreakableLines(sourceActor.id)
        );
        return allSourceActorsProcessed;
      }

      if (!getBreakableLines(location.source.id)) {
        return false;
      }

      // Also ensure that CodeMirror updated its content
      return getEditorContent(dbg) !== DEBUGGER_L10N.getStr("loadingText");
    },
    "selected source"
  );
}

/**
 * The generated source of WASM source are WASM binary file,
 * which have many broken/disabled features in the debugger.
 *
 * They especially have a very special behavior in CodeMirror
 * where line labels aren't line number, but hex addresses.
 */

function isWasmBinarySource(source) {
  return source.isWasm && !source.isOriginal;
}

function getVisibleSelectedFrameLine(dbg) {
  const frame = dbg.selectors.getVisibleSelectedFrame();
  return frame?.location.line;
}

function getVisibleSelectedFrameColumn(dbg) {
  const frame = dbg.selectors.getVisibleSelectedFrame();
  return frame?.location.column;
}

/**
 * Assert that a given line is breakable or not.
 * Verify that CodeMirror gutter is grayed out via the empty line classname if not breakable.
 */

async function assertLineIsBreakable(dbg, file, line, shouldBeBreakable) {
  const el = await getNodeAtEditorGutterLine(dbg, line);
  const lineText = `${line}| ${el.innerText.substring(0, 50)}${
    el.innerText.length > 50 ? "…" : ""
  } — in ${file}`;
  // When a line is not breakable, the "empty-line" class is added
  // and the line is greyed out
  if (shouldBeBreakable) {
    ok(!el.classList.contains("empty-line"), `${lineText} should be breakable`);
  } else {
    ok(
      el.classList.contains("empty-line"),
      `${lineText} should NOT be breakable`
    );
  }
}

/**
 * Assert that the debugger is highlighting the correct location.
 *
 * @memberof mochitest/asserts
 * @param {Object} dbg
 * @param {String} source
 * @param {Number} line
 * @static
 */

function assertHighlightLocation(dbg, source, line) {
  source = findSource(dbg, source);

  // Check the selected source
  is(
    dbg.selectors.getSelectedSource().url,
    source.url,
    "source url is correct"
  );

  // Check the highlight line
  const lineEl = findElement(dbg, "highlightLine");
  ok(lineEl, "Line is highlighted");

  is(
    findAllElements(dbg, "highlightLine").length,
    1,
    "Only 1 line is highlighted"
  );

  ok(isVisibleInEditor(dbg, lineEl), "Highlighted line is visible");

  const lineInfo = getCMEditor(dbg).lineInfo(isCm6Enabled ? line : line - 1);
  ok(lineInfo.wrapClass.includes("highlight-line"), "Line is highlighted");
}

/**
 * Helper function for assertPausedAtSourceAndLine.
 *
 * Assert that CodeMirror reports to be paused at the given line/column.
 */

async function _assertDebugLine(dbg, line, column) {
  const source = dbg.selectors.getSelectedSource();
  // WASM lines are hex addresses which have to be mapped to decimal line number
  if (isWasmBinarySource(source)) {
    line = wasmOffsetToLine(dbg, source.id, line);
  }

  // Check the debug line
  // cm6 lines are 1-based, while cm5 are 0-based, to keep compatibility with
  // .lineInfo usage in other locations.
  const lineInfo = getCMEditor(dbg).lineInfo(isCm6Enabled ? line : line - 1);
  const sourceTextContent = dbg.selectors.getSelectedSourceTextContent();
  if (source && !sourceTextContent) {
    const url = source.url;
    ok(
      false,
      `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.`
    );
    return;
  }

  // Scroll the line into view to make sure the content
  // on the line is rendered and in the dom.
  await scrollEditorIntoView(dbg, line, 0);

  if (!lineInfo.wrapClass) {
    const pauseLine = getVisibleSelectedFrameLine(dbg);
    ok(false, `Expected pause line on line ${line}, it is on ${pauseLine}`);
    return;
  }

  ok(
    lineInfo?.wrapClass.includes("new-debug-line"),
    `Line ${line} is not highlighted as paused`
  );

  const debugLine =
    findElement(dbg, "debugLine") || findElement(dbg, "debugErrorLine");

  is(
    findAllElements(dbg, "debugLine").length +
      findAllElements(dbg, "debugErrorLine").length,
    1,
    "There is only one line"
  );

  ok(isVisibleInEditor(dbg, debugLine), "debug line is visible");

  const markedSpans = lineInfo.handle.markedSpans;
  if (markedSpans && markedSpans.length && !isWasmBinarySource(source)) {
    const hasExpectedDebugLine = markedSpans.some(
      span =>
        span.marker.className?.includes("debug-expression") &&
        // When a precise column is expected, ensure that we have at least
        // one "debug line" for the column we expect.
        // (See the React Component: DebugLine.setDebugLine)
        (!column || span.from == column)
    );
    ok(
      hasExpectedDebugLine,
      "Got the expected DebugLine. i.e. got the right marker in codemirror visualizing the breakpoint"
    );
  }
  info(`Paused on line ${line}`);
}

/**
 * Make sure the debugger is paused at a certain source ID and line.
 *
 * @param {Object} dbg
 * @param {String} expectedSourceId
 * @param {Number} expectedLine
 * @param {Number} [expectedColumn]
 */

async function assertPausedAtSourceAndLine(
  dbg,
  expectedSourceId,
  expectedLine,
  expectedColumn
) {
  // Check that the debugger is paused.
  assertPaused(dbg);

  // Check that the paused location is correctly rendered.
  ok(isSelectedFrameSelected(dbg), "top frame's source is selected");

  // Check the pause location
  const pauseLine = getVisibleSelectedFrameLine(dbg);
  is(
    pauseLine,
    expectedLine,
    "Redux state for currently selected frame's line is correct"
  );
  const pauseColumn = getVisibleSelectedFrameColumn(dbg);
  if (expectedColumn) {
    // `pauseColumn` is 0-based, coming from internal state,
    // while `expectedColumn` is manually passed from test scripts and so is 1-based.
    is(
      pauseColumn + 1,
      expectedColumn,
      "Redux state for currently selected frame's column is correct"
    );
  }
  await _assertDebugLine(dbg, pauseLine, pauseColumn);

  ok(isVisibleInEditor(dbg, findElement(dbg, "gutters")), "gutter is visible");

  const frames = dbg.selectors.getCurrentThreadFrames();
  const selectedSource = dbg.selectors.getSelectedSource();

  // WASM support is limited when we are on the generated binary source
  if (isWasmBinarySource(selectedSource)) {
    return;
  }

  ok(frames.length >= 1, "Got at least one frame");

  // Lets make sure we can assert both original and generated file locations when needed
  const { source, line, column } = isGeneratedId(expectedSourceId)
    ? frames[0].generatedLocation
    : frames[0].location;
  is(source.id, expectedSourceId, "Frame has correct source");
  is(
    line,
    expectedLine,
    `Frame paused at line ${line}, but expected line ${expectedLine}`
  );

  if (expectedColumn) {
    // `column` is 0-based, coming from internal state,
    // while `expectedColumn` is manually passed from test scripts and so is 1-based.
    is(
      column + 1,
      expectedColumn,
      `Frame paused at column ${
        column + 1
      }, but expected column ${expectedColumn}`
    );
  }
}

async function waitForThreadCount(dbg, count) {
  return waitForState(
    dbg,
    state => dbg.selectors.getThreads(state).length == count
  );
}

async function waitForLoadedScopes(dbg) {
  const scopes = await waitForElement(dbg, "scopes");
  // Since scopes auto-expand, we can assume they are loaded when there is a tree node
  // with the aria-level attribute equal to "2".
  await waitUntil(() => scopes.querySelector('.tree-node[aria-level="2"]'));
}

function waitForBreakpointCount(dbg, count) {
  return waitForState(dbg, () => dbg.selectors.getBreakpointCount() == count);
}

function waitForBreakpoint(dbg, url, line) {
  return waitForState(dbg, () => findBreakpoint(dbg, url, line));
}

function waitForBreakpointRemoved(dbg, url, line) {
  return waitForState(dbg, () => !findBreakpoint(dbg, url, line));
}

/**
 * Returns boolean for whether the debugger is paused.
 *
 * @param {Object} dbg
 */

function isPaused(dbg) {
  return dbg.selectors.getIsCurrentThreadPaused();
}

/**
 * Assert that the debugger is not currently paused.
 *
 * @param {Object} dbg
 * @param {String} msg
 *        Optional assertion message
 */

function assertNotPaused(dbg, msg = "client is not paused") {
  ok(!isPaused(dbg), msg);
}

/**
 * Assert that the debugger is currently paused.
 *
 * @param {Object} dbg
 */

function assertPaused(dbg, msg = "client is paused") {
  ok(isPaused(dbg), msg);
}

/**
 * Waits for the debugger to be fully paused.
 *
 * @param {Object} dbg
 * @param {String} url
 *        Optional URL of the script we should be pausing on.
 * @param {Object} options
 *         {Boolean} shouldWaitForLoadScopes
 *        When paused in original files with original variable mapping disabled, scopes are
 *        not going to exist, lets not wait for it. defaults to true
 */

async function waitForPaused(
  dbg,
  url,
  options = { shouldWaitForLoadedScopes: true }
) {
  info("Waiting for the debugger to pause");
  const { getSelectedScope, getCurrentThread, getCurrentThreadFrames } =
    dbg.selectors;

  await waitForState(
    dbg,
    () => isPaused(dbg) && !!getSelectedScope(getCurrentThread()),
    "paused"
  );

  await waitForState(dbg, getCurrentThreadFrames, "fetched frames");

  if (options.shouldWaitForLoadedScopes) {
    await waitForLoadedScopes(dbg);
  }

  // Note that this will wait for symbols, which are fetched on pause
  await waitForSelectedSource(dbg, url);
}

/**
 * Waits for the debugger to resume.
 *
 * @param {Objeect} dbg
 */

function waitForResumed(dbg) {
  info("Waiting for the debugger to resume");
  return waitForState(dbg, () => !dbg.selectors.getIsCurrentThreadPaused());
}

function waitForInlinePreviews(dbg) {
  return waitForState(dbg, () => dbg.selectors.getInlinePreviews());
}

function waitForCondition(dbg, condition) {
  return waitForState(dbg, () =>
    dbg.selectors
      .getBreakpointsList()
      .find(bp => bp.options.condition == condition)
  );
}

function waitForLog(dbg, logValue) {
  return waitForState(dbg, () =>
    dbg.selectors
      .getBreakpointsList()
      .find(bp => bp.options.logValue == logValue)
  );
}

async function waitForPausedThread(dbg, thread) {
  return waitForState(dbg, () => dbg.selectors.getIsPaused(thread));
}

function isSelectedFrameSelected(dbg) {
  const frame = dbg.selectors.getVisibleSelectedFrame();

  // Make sure the source text is completely loaded for the
  // source we are paused in.
  const source = dbg.selectors.getSelectedSource();
  const sourceTextContent = dbg.selectors.getSelectedSourceTextContent();

  if (!source || !sourceTextContent) {
    return false;
  }

  return source.id == frame.location.source.id;
}

/**
 * Checks to see if the frame is selected and the displayed title is correct.
 *
 * @param {Object} dbg
 * @param {DOM Node} frameElement
 * @param {String} expectedTitle
 */

function assertFrameIsSelected(dbg, frameElement, expectedTitle) {
  const selectedFrame = dbg.selectors.getSelectedFrame();
  ok(frameElement.classList.contains("selected"), "The frame is selected");
  is(
    frameElement.querySelector(".title").innerText,
    expectedTitle,
    "The selected frame element has the expected title"
  );
  // For `<anonymous>` frames, there is likely no displayName
  is(
    selectedFrame.displayName,
    expectedTitle == "" ? undefined : expectedTitle,
    "The selected frame has the correct display title"
  );
}

/**
 * Checks to see if the frame is  not selected.
 *
 * @param {Object} dbg
 * @param {DOM Node} frameElement
 * @param {String} expectedTitle
 */

function assertFrameIsNotSelected(dbg, frameElement, expectedTitle) {
  const selectedFrame = dbg.selectors.getSelectedFrame();
  ok(!frameElement.classList.contains("selected"), "The frame is selected");
  is(
    frameElement.querySelector(".title").innerText,
    expectedTitle,
    "The selected frame element has the expected title"
  );
}

/**
 *  Clear all the debugger related preferences.
 */

async function clearDebuggerPreferences(prefs = []) {
  resetSchemaVersion();
  asyncStorage.clear();
  Services.prefs.clearUserPref("devtools.debugger.alphabetize-outline");
  Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions");
  Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions");
  Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions");
  Services.prefs.clearUserPref("devtools.debugger.pending-selected-location");
  Services.prefs.clearUserPref("devtools.debugger.expressions");
  Services.prefs.clearUserPref("devtools.debugger.breakpoints-visible");
  Services.prefs.clearUserPref("devtools.debugger.call-stack-visible");
  Services.prefs.clearUserPref("devtools.debugger.scopes-visible");
  Services.prefs.clearUserPref("devtools.debugger.skip-pausing");

  for (const pref of prefs) {
    await pushPref(...pref);
  }
}

/**
 * Intilializes the debugger.
 *
 * @memberof mochitest
 * @param {String} url
 * @return {Promise} dbg
 * @static
 */


async function initDebugger(url, ...sources) {
  // We depend on EXAMPLE_URLs origin to do cross origin/process iframes via
  // EXAMPLE_REMOTE_URL. If the top level document origin changes,
  // we may break this. So be careful if you want to change EXAMPLE_URL.
  return initDebuggerWithAbsoluteURL(EXAMPLE_URL + url, ...sources);
}

async function initDebuggerWithAbsoluteURL(url, ...sources) {
  await clearDebuggerPreferences();
  const toolbox = await openNewTabAndToolbox(url, "jsdebugger");
  const dbg = createDebuggerContext(toolbox);

  await waitForSources(dbg, ...sources);
  return dbg;
}

async function initPane(url, pane, prefs) {
  await clearDebuggerPreferences(prefs);
  return openNewTabAndToolbox(EXAMPLE_URL + url, pane);
}

/**
 * Returns a source that matches a given filename, or a URL.
 * This also accept a source as input argument, in such case it just returns it.
 *
 * @param {Object} dbg
 * @param {String} filenameOrUrlOrSource
 *        The typical case will be to pass only a filename,
 *        but you may also pass a full URL to match sources without filesnames like data: URL
 *        or pass the source itself, which is just returned.
 * @param {Object} options
 * @param {Boolean} options.silent
 *        If true, won't throw if the source is missing.
 * @return {Object} source
 */

function findSource(
  dbg,
  filenameOrUrlOrSource,
  { silent } = { silent: false }
) {
  if (typeof filenameOrUrlOrSource !== "string") {
    // Support passing in a source object itself all APIs that use this
    // function support both styles
    return filenameOrUrlOrSource;
  }

  const sources = dbg.selectors.getSourceList();
  const source = sources.find(s => {
    // Sources don't have a file name attribute, we need to compute it here:
    const sourceFileName = s.url
      ? getUnicodeUrlPath(s.url.substring(s.url.lastIndexOf("/") + 1))
      : "";

    // The input argument may either be only the filename, or the complete URL
    // This helps match sources whose URL doesn't contain a filename, like data: URLs
    return (
      sourceFileName == filenameOrUrlOrSource || s.url == filenameOrUrlOrSource
    );
  });

  if (!source) {
    if (silent) {
      return false;
    }

    throw new Error(`Unable to find source: ${filenameOrUrlOrSource}`);
  }

  return source;
}

function findSourceContent(dbg, url, opts) {
  const source = findSource(dbg, url, opts);

  if (!source) {
    return null;
  }
  const content = dbg.selectors.getSettledSourceTextContent(
    createLocation({
      source,
    })
  );

  if (!content) {
    return null;
  }

  if (content.state !== "fulfilled") {
    throw new Error(`Expected loaded source, got${content.value}`);
  }

  return content.value;
}

function sourceExists(dbg, url) {
  return !!findSource(dbg, url, { silent: true });
}

function waitForLoadedSource(dbg, url) {
  return waitForState(
    dbg,
    () => {
      const source = findSource(dbg, url, { silent: true });
      return (
        source &&
        dbg.selectors.getSettledSourceTextContent(
          createLocation({
            source,
          })
        )
      );
    },
    "loaded source"
  );
}

/*
 * Selects the source node for a specific source
 * from the source tree.
 *
 * @param {Object} dbg
 * @param {String} filename - The filename for the specific source
 * @param {Number} sourcePosition - The source node postion in the tree
 * @param {String} message - The info message to display
 */

async function selectSourceFromSourceTree(
  dbg,
  fileName,
  sourcePosition,
  message
) {
  info(message);
  await clickElement(dbg, "sourceNode", sourcePosition);
  await waitForSelectedSource(dbg, fileName);
  await waitFor(
    () => getEditorContent(dbg) !== `Loading…`,
    "Wait for source to completely load"
  );
}

/*
 * Trigger a context menu in the debugger source tree
 *
 * @param {Object} dbg
 * @param {Obejct} sourceTreeNode - The node in the source tree which the context menu
 *                                  item needs to be triggered on.
 * @param {String} contextMenuItem - The id for the context menu item to be selected
 */

async function triggerSourceTreeContextMenu(
  dbg,
  sourceTreeNode,
  contextMenuItem
) {
  const onContextMenu = waitForContextMenu(dbg);
  rightClickEl(dbg, sourceTreeNode);
  const menupopup = await onContextMenu;
  const onHidden = new Promise(resolve => {
    menupopup.addEventListener("popuphidden", resolve, { once: true });
  });
  selectContextMenuItem(dbg, contextMenuItem);
  await onHidden;
}

/**
 * Selects the source.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {String} url
 * @param {Number} line
 * @param {Number} column
 * @return {Promise}
 * @static
 */

async function selectSource(dbg, url, line, column) {
  const source = findSource(dbg, url);

  await dbg.actions.selectLocation(createLocation({ source, line, column }), {
    keepContext: false,
  });
  return waitForSelectedSource(dbg, source);
}

async function closeTab(dbg, url) {
  await dbg.actions.closeTab(findSource(dbg, url));
}

function countTabs(dbg) {
  return findElement(dbg, "sourceTabs").children.length;
}

/**
 * Steps over.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {Object} pauseOptions
 * @return {Promise}
 * @static
 */

async function stepOver(dbg, pauseOptions) {
  const pauseLine = getVisibleSelectedFrameLine(dbg);
  info(`Stepping over from ${pauseLine}`);
  await dbg.actions.stepOver();
  return waitForPaused(dbg, null, pauseOptions);
}

/**
 * Steps in.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @return {Promise}
 * @static
 */

async function stepIn(dbg) {
  const pauseLine = getVisibleSelectedFrameLine(dbg);
  info(`Stepping in from ${pauseLine}`);
  await dbg.actions.stepIn();
  return waitForPaused(dbg);
}

/**
 * Steps out.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @return {Promise}
 * @static
 */

async function stepOut(dbg) {
  const pauseLine = getVisibleSelectedFrameLine(dbg);
  info(`Stepping out from ${pauseLine}`);
  await dbg.actions.stepOut();
  return waitForPaused(dbg);
}

/**
 * Resumes.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @return {Promise}
 * @static
 */

async function resume(dbg) {
  const pauseLine = getVisibleSelectedFrameLine(dbg);
  info(`Resuming from ${pauseLine}`);
  const onResumed = waitForResumed(dbg);
  await dbg.actions.resume();
  return onResumed;
}

function deleteExpression(dbg, input) {
  info(`Delete expression "${input}"`);
  return dbg.actions.deleteExpression({ input });
}

/**
 * Reloads the debuggee.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {Array} sources
 * @return {Promise}
 * @static
 */

async function reload(dbg, ...sources) {
  await reloadBrowser();
  return waitForSources(dbg, ...sources);
}

// Only use this method when the page is paused by the debugger
// during page load and we navigate away without resuming.
//
// In this particular scenario, the page will never be "loaded".
// i.e. emit DOCUMENT_EVENT's dom-complete
// And consequently, debugger panel won't emit "reloaded" event.
async function reloadWhenPausedBeforePageLoaded(dbg, ...sources) {
  // But we can at least listen for the next DOCUMENT_EVENT's dom-loading,
  // which should be fired even if the page is pause the earliest.
  const { resourceCommand } = dbg.commands;
  const { onResource: onTopLevelDomLoading } =
    await resourceCommand.waitForNextResource(
      resourceCommand.TYPES.DOCUMENT_EVENT,
      {
        ignoreExistingResources: true,
        predicate: resource =>
          resource.targetFront.isTopLevel && resource.name === "dom-loading",
      }
    );

  gBrowser.reloadTab(gBrowser.selectedTab);

  info("Wait for DOCUMENT_EVENT dom-loading after reload");
  await onTopLevelDomLoading;
  return waitForSources(dbg, ...sources);
}

/**
 * Navigates the debuggee to another url.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {String} url
 * @param {Array} sources
 * @return {Promise}
 * @static
 */

async function navigate(dbg, url, ...sources) {
  return navigateToAbsoluteURL(dbg, EXAMPLE_URL + url, ...sources);
}

/**
 * Navigates the debuggee to another absolute url.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {String} url
 * @param {Array} sources
 * @return {Promise}
 * @static
 */

async function navigateToAbsoluteURL(dbg, url, ...sources) {
  await navigateTo(url);
  return waitForSources(dbg, ...sources);
}

function getFirstBreakpointColumn(dbg, source, line) {
  const position = dbg.selectors.getFirstBreakpointPosition(
    createLocation({
      line,
      source,
    })
  );

  return getSelectedLocation(position, source).column;
}

function isMatchingLocation(location1, location2) {
  return (
    location1?.source.id == location2?.source.id &&
    location1?.line == location2?.line &&
    location1?.column == location2?.column
  );
}

function getBreakpointForLocation(dbg, location) {
  if (!location) {
    return undefined;
  }

  const isGeneratedSource = isGeneratedId(location.source.id);
  return dbg.selectors.getBreakpointsList().find(bp => {
    const loc = isGeneratedSource ? bp.generatedLocation : bp.location;
    return isMatchingLocation(loc, location);
  });
}

/**
 * Adds a breakpoint to a source at line/col.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {String} source
 * @param {Number} line
 * @param {Number} col
 * @return {Promise}
 * @static
 */

async function addBreakpoint(dbg, source, line, column, options) {
  source = findSource(dbg, source);
  const bpCount = dbg.selectors.getBreakpointCount();
  const onBreakpoint = waitForDispatch(dbg.store, "SET_BREAKPOINT");
  await dbg.actions.addBreakpoint(
    // column is 0-based internally, but tests are using 1-based.
    createLocation({ source, line, column: column - 1 }),
    options
  );
  await onBreakpoint;
  is(
    dbg.selectors.getBreakpointCount(),
    bpCount + 1,
    "a new breakpoint was created"
  );
}

// use shortcut to open conditional panel.
function setConditionalBreakpointWithKeyboardShortcut(dbg, condition) {
  pressKey(dbg, "toggleCondPanel");
  return typeInPanel(dbg, condition);
}

/**
 * Similar to `addBreakpoint`, but uses the UI instead or calling
 * the actions directly. This only support breakpoint on lines,
 * not on a specific column.
 */

async function addBreakpointViaGutter(dbg, line) {
  info(`Add breakpoint via the editor on line ${line}`);
  await clickGutter(dbg, line);
  return waitForDispatch(dbg.store, "SET_BREAKPOINT");
}

async function removeBreakpointViaGutter(dbg, line) {
  const onRemoved = waitForDispatch(dbg.store, "REMOVE_BREAKPOINT");
  await clickGutter(dbg, line);
  await onRemoved;
}

function disableBreakpoint(dbg, source, line, column) {
  if (column === 0) {
    throw new Error("disableBreakpoint expect a 1-based column argument");
  }
  // `internalColumn` is 0-based internally, while `column` manually defined in test scripts is 1-based.
  const internalColumn = column
    ? column - 1
    : getFirstBreakpointColumn(dbg, source, line);
  const location = createLocation({
    source,
    line,
    column: internalColumn,
  });
  const bp = getBreakpointForLocation(dbg, location);
  return dbg.actions.disableBreakpoint(bp);
}

function findBreakpoint(dbg, url, line) {
  const source = findSource(dbg, url);
  return dbg.selectors.getBreakpointsForSource(source, line)[0];
}

// helper for finding column breakpoints.
function findColumnBreakpoint(dbg, url, line, column) {
  const source = findSource(dbg, url);
  const lineBreakpoints = dbg.selectors.getBreakpointsForSource(source, line);

  return lineBreakpoints.find(bp => {
    return source.isOriginal
      ? bp.location.column === column
      : bp.generatedLocation.column === column;
  });
}

async function loadAndAddBreakpoint(dbg, filename, line, column) {
  const {
    selectors: { getBreakpoint, getBreakpointCount, getBreakpointsMap },
  } = dbg;

  await waitForSources(dbg, filename);

  ok(true"Original sources exist");
  const source = findSource(dbg, filename);

  await selectSource(dbg, source);

  // Test that breakpoint is not off by a line.
  await addBreakpoint(dbg, source, line, column);

  is(getBreakpointCount(), 1, "One breakpoint exists");
  // column is 0-based internally, but tests are using 1-based.
  if (!getBreakpoint(createLocation({ source, line, column: column - 1 }))) {
    const breakpoints = getBreakpointsMap();
    const id = Object.keys(breakpoints).pop();
    const loc = breakpoints[id].location;
    ok(
      false,
      `Breakpoint has correct line ${line}, column ${column}, but was line ${
        loc.line
      } column ${loc.column + 1}`
    );
  }

  return source;
}

async function invokeWithBreakpoint(
  dbg,
  fnName,
  filename,
  { line, column },
  handler,
  pauseOptions
) {
  const source = await loadAndAddBreakpoint(dbg, filename, line, column);

  const invokeResult = invokeInTab(fnName);

  const invokeFailed = await Promise.race([
    waitForPaused(dbg, null, pauseOptions),
    invokeResult.then(
      () => new Promise(() => {}),
      () => true
    ),
  ]);

  if (invokeFailed) {
    await invokeResult;
    return;
  }

  await assertPausedAtSourceAndLine(
    dbg,
    findSource(dbg, filename).id,
    line,
    column
  );

  await removeBreakpoint(dbg, source.id, line, column);

  is(dbg.selectors.getBreakpointCount(), 0, "Breakpoint reverted");

  await handler(source);

  await resume(dbg);

  // eslint-disable-next-line max-len
  // If the invoke errored later somehow, capture here so the error is reported nicely.
  await invokeResult;
}

function prettyPrint(dbg) {
  const source = dbg.selectors.getSelectedSource();
  return dbg.actions.prettyPrintAndSelectSource(source);
}

async function expandAllScopes(dbg) {
  const scopes = await waitForElement(dbg, "scopes");
  const scopeElements = scopes.querySelectorAll(
    '.tree-node[aria-level="1"][data-expandable="true"]:not([aria-expanded="true"])'
  );
  const indices = Array.from(scopeElements, el => {
    return Array.prototype.indexOf.call(el.parentNode.childNodes, el);
  }).reverse();

  for (const index of indices) {
    await toggleScopeNode(dbg, index + 1);
  }
}

async function assertScopes(dbg, items) {
  await expandAllScopes(dbg);

  for (const [i, val] of items.entries()) {
    if (Array.isArray(val)) {
      is(getScopeNodeLabel(dbg, i + 1), val[0]);
      is(
        getScopeNodeValue(dbg, i + 1),
        val[1],
        `"${val[0]}" has the expected "${val[1]}" value`
      );
    } else {
      is(getScopeNodeLabel(dbg, i + 1), val);
    }
  }

  is(getScopeNodeLabel(dbg, items.length + 1), "Window");
}

function findSourceTreeThreadByName(dbg, name) {
  return [...findAllElements(dbg, "sourceTreeThreads")].find(el => {
    return el.textContent.includes(name);
  });
}

function findSourceTreeGroupByName(dbg, name) {
  return [...findAllElements(dbg, "sourceTreeGroups")].find(el => {
    return el.textContent.includes(name);
  });
}

function findSourceNodeWithText(dbg, text) {
  return [...findAllElements(dbg, "sourceNodes")].find(el => {
    return el.textContent.includes(text);
  });
}

/**
 * Assert the icon type used in the SourceTree for a given source
 *
 * @param {Object} dbg
 * @param {String} sourceName
 *        Name of the source displayed in the source tree
 * @param {String} icon
 *        Expected icon CSS classname
 */

function assertSourceIcon(dbg, sourceName, icon) {
  const sourceItem = findSourceNodeWithText(dbg, sourceName);
  ok(sourceItem, `Found the source item for ${sourceName}`);
  is(
    sourceItem.querySelector(".source-icon").className,
    `img source-icon ${icon}`,
    `The icon for ${sourceName} is correct`
  );
}

async function expandSourceTree(dbg) {
  // Click on expand all context menu for all top level "expandable items".
  // If there is no project root, it will be thread items.
  // But when there is a project root, it can be directory or group items.
  // Select only expandable in order to ignore source items.
  for (const rootNode of dbg.win.document.querySelectorAll(
    ".sources-list > .tree > .tree-node[data-expandable=true]"
  )) {
    await expandAllSourceNodes(dbg, rootNode);
  }
}

async function expandAllSourceNodes(dbg, treeNode) {
  return triggerSourceTreeContextMenu(dbg, treeNode, "#node-menu-expand-all");
}

/**
 * Removes a breakpoint from a source at line/col.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {String} source
 * @param {Number} line
 * @param {Number} col
 * @return {Promise}
 * @static
 */

function removeBreakpoint(dbg, sourceId, line, column) {
  const source = dbg.selectors.getSource(sourceId);
  // column is 0-based internally, but tests are using 1-based.
  column = column ? column - 1 : getFirstBreakpointColumn(dbg, source, line);
  const location = createLocation({
    source,
    line,
    column,
  });
  const bp = getBreakpointForLocation(dbg, location);
  return dbg.actions.removeBreakpoint(bp);
}

/**
 * Toggles the Pause on exceptions feature in the debugger.
 *
 * @memberof mochitest/actions
 * @param {Object} dbg
 * @param {Boolean} pauseOnExceptions
 * @param {Boolean} pauseOnCaughtExceptions
 * @return {Promise}
 * @static
 */

async function togglePauseOnExceptions(
  dbg,
  pauseOnExceptions,
  pauseOnCaughtExceptions
) {
  return dbg.actions.pauseOnExceptions(
    pauseOnExceptions,
    pauseOnCaughtExceptions
  );
}

// Helpers

/**
 * Invokes a global function in the debuggee tab.
 *
 * @memberof mochitest/helpers
 * @param {String} fnc The name of a global function on the content window to
 *                     call. This is applied to structured clones of the
 *                     remaining arguments to invokeInTab.
 * @param {Any} ...args Remaining args to serialize and pass to fnc.
 * @return {Promise}
 * @static
 */

function invokeInTab(fnc, ...args) {
  info(`Invoking in tab: ${fnc}(${args.map(uneval).join(",")})`);
  return ContentTask.spawn(gBrowser.selectedBrowser, { fnc, args }, options =>
    content.wrappedJSObject[options.fnc](...options.args)
  );
}

function clickElementInTab(selector) {
  info(`click element ${selector} in tab`);

  return SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [selector],
    function (_selector) {
      const element = content.document.querySelector(_selector);
      // Run the click in another event loop in order to immediately resolve spawn's promise.
      // Otherwise if we pause on click and navigate, the JSWindowActor used by spawn will
      // be destroyed while its query is still pending. And this would reject the promise.
      content.setTimeout(() => {
        element.click();
      });
    }
  );
}

const isLinux = Services.appinfo.OS === "Linux";
const isMac = Services.appinfo.OS === "Darwin";
const cmdOrCtrl = isMac ? { metaKey: true } : { ctrlKey: true };
const shiftOrAlt = isMac
  ? { accelKey: true, shiftKey: true }
  : { accelKey: true, altKey: true };

const cmdShift = isMac
  ? { accelKey: true, shiftKey: true, metaKey: true }
  : { accelKey: true, shiftKey: true, ctrlKey: true };

// On Mac, going to beginning/end only works with meta+left/right.  On
// Windows, it only works with home/end.  On Linux, apparently, either
// ctrl+left/right or home/end work.
const endKey = isMac
  ? { code: "VK_RIGHT", modifiers: cmdOrCtrl }
  : { code: "VK_END" };
const startKey = isMac
  ? { code: "VK_LEFT", modifiers: cmdOrCtrl }
  : { code: "VK_HOME" };

const keyMappings = {
  close: { code: "w", modifiers: cmdOrCtrl },
  commandKeyDown: { code: "VK_META", modifiers: { type: "keydown" } },
  commandKeyUp: { code: "VK_META", modifiers: { type: "keyup" } },
  debugger: { code: "s", modifiers: shiftOrAlt },
  // test conditional panel shortcut
  toggleCondPanel: { code: "b", modifiers: cmdShift },
  toggleLogPanel: { code: "y", modifiers: cmdShift },
  toggleBreakpoint: { code: "b", modifiers: cmdOrCtrl },
  inspector: { code: "c", modifiers: shiftOrAlt },
  quickOpen: { code: "p", modifiers: cmdOrCtrl },
  quickOpenFunc: { code: "o", modifiers: cmdShift },
  quickOpenLine: { code: ":", modifiers: cmdOrCtrl },
  fileSearch: { code: "f", modifiers: cmdOrCtrl },
  projectSearch: { code: "f", modifiers: cmdShift },
  fileSearchNext: { code: "g", modifiers: { metaKey: true } },
  fileSearchPrev: { code: "g", modifiers: cmdShift },
  goToLine: { code: "g", modifiers: { ctrlKey: true } },
  Enter: { code: "VK_RETURN" },
  ShiftEnter: { code: "VK_RETURN", modifiers: { shiftKey: true } },
  AltEnter: {
    code: "VK_RETURN",
    modifiers: { altKey: true },
  },
  Space: { code: "VK_SPACE" },
  Up: { code: "VK_UP" },
  Down: { code: "VK_DOWN" },
  Right: { code: "VK_RIGHT" },
  Left: { code: "VK_LEFT" },
  End: endKey,
  Start: startKey,
  Tab: { code: "VK_TAB" },
  ShiftTab: { code: "VK_TAB", modifiers: { shiftKey: true } },
  Escape: { code: "VK_ESCAPE" },
  Delete: { code: "VK_DELETE" },
  pauseKey: { code: "VK_F8" },
  resumeKey: { code: "VK_F8" },
  stepOverKey: { code: "VK_F10" },
  stepInKey: { code: "VK_F11" },
  stepOutKey: {
    code: "VK_F11",
    modifiers: { shiftKey: true },
  },
  Backspace: { code: "VK_BACK_SPACE" },
};

/**
 * Simulates a key press in the debugger window.
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {String} keyName
 * @return {Promise}
 * @static
 */

function pressKey(dbg, keyName) {
  const keyEvent = keyMappings[keyName];
  const { code, modifiers } = keyEvent;
  info(`The ${keyName} key is pressed`);
  return EventUtils.synthesizeKey(code, modifiers || {}, dbg.win);
}

function type(dbg, string) {
  string.split("").forEach(char => EventUtils.synthesizeKey(char, {}, dbg.win));
}

/*
 * Checks to see if the inner element is visible inside the editor.
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {HTMLElement} inner element
 * @return {boolean}
 * @static
 */


function isVisibleInEditor(dbg, element) {
  return isVisible(findElement(dbg, "codeMirror"), element);
}

/*
 * Checks to see if the inner element is visible inside the
 * outer element.
 *
 * Note, the inner element does not need to be entirely visible,
 * it is possible for it to be somewhat clipped by the outer element's
 * bounding element or for it to span the entire length, starting before the
 * outer element and ending after.
 *
 * @memberof mochitest/helpers
 * @param {HTMLElement} outer element
 * @param {HTMLElement} inner element
 * @return {boolean}
 * @static
 */

function isVisible(outerEl, innerEl) {
  if (!innerEl || !outerEl) {
    return false;
  }

  const innerRect = innerEl.getBoundingClientRect();
  const outerRect = outerEl.getBoundingClientRect();

  const verticallyVisible =
    innerRect.top >= outerRect.top ||
    innerRect.bottom <= outerRect.bottom ||
    (innerRect.top < outerRect.top && innerRect.bottom > outerRect.bottom);

  const horizontallyVisible =
    innerRect.left >= outerRect.left ||
    innerRect.right <= outerRect.right ||
    (innerRect.left < outerRect.left && innerRect.right > outerRect.right);

  const visible = verticallyVisible && horizontallyVisible;
  return visible;
}

/**
 * Get the element in the editor gutter at the specified line
 * @param {Object} dbg
 * @param {Number} line
 * @returns
 */

async function getEditorLineGutter(dbg, line) {
  let el = await codeMirrorGutterElement(dbg, line);
  while (el && !el.matches(".CodeMirror-code > div")) {
    el = el.parentElement;
  }

  return el;
}

// Handles virtualization scenarios
async function scrollAndGetEditorLineGutterElement(dbg, line) {
  const editor = getCMEditor(dbg);
  await scrollEditorIntoView(dbg, line, 0);
  const selectedSource = dbg.selectors.getSelectedSource();
  // For WASM sources get the hexadecimal line number displayed in the gutter
  if (editor.isWasm && !selectedSource.isOriginal) {
    const wasmLineFormatter = editor.getWasmLineNumberFormatter();
    line = wasmLineFormatter(line);
  }

  const els = findAllElementsWithSelector(
    dbg,
    isCm6Enabled
      ? ".cm-gutter.cm-lineNumbers .cm-gutterElement"
      : ".CodeMirror-code .CodeMirror-linenumber"
  );
  return [...els].find(el => el.innerText == line);
}

/**
 * Gets node at a specific line in the editor
 * @param {*} dbg
 * @param {Number} line
 * @returns {Element} DOM Element
 */

async function getNodeAtEditorLine(dbg, line) {
  if (isCm6Enabled) {
    // To handle virtualized lines accurately, lets use the
    // cm6 utility here.
    await scrollEditorIntoView(dbg, line, 0);
    return getCMEditor(dbg).getElementAtLine(line);
  }
  return getEditorLineGutter(dbg, line);
}

/**
 * Gets node at a specific line in the gutter
 * @param {*} dbg
 * @param {Number} line
 * @returns {Element} DOM Element
 */

async function getNodeAtEditorGutterLine(dbg, line) {
  if (isCm6Enabled) {
    return scrollAndGetEditorLineGutterElement(dbg, line);
  }
  // Note: In CM5 both the line gutter elements and the
  // line content elements are within the editor line.
  return getEditorLineGutter(dbg, line);
}

async function getConditionalPanelAtLine(dbg, line) {
  info(`Get conditional panel at line ${line}`);
  let el = await getNodeAtEditorLine(dbg, line);
  if (isCm6Enabled) {
    // In CM6 the conditional panel for a specific line
    // is injected in a sibling node just after.
    el = el.nextSibling;
  }
  return el.querySelector(".conditional-breakpoint-panel");
}

async function waitForConditionalPanelFocus(dbg) {
  if (isCm6Enabled) {
    return waitFor(
      () =>
        dbg.win.document.activeElement.classList.contains("cm-content") &&
        dbg.win.document.activeElement.closest(".conditional-breakpoint-panel")
    );
  }
  return waitFor(() => dbg.win.document.activeElement.tagName === "TEXTAREA");
}

/**
 * Opens the debugger editor context menu in either codemirror or the
 * the debugger gutter.
 * @param {Object} dbg
 * @param {String} elementName
 *                  The element to select
 * @param {Number} line
 *                  The line to open the context menu on.
 */

async function openContextMenuInDebugger(dbg, elementName, line) {
  const waitForOpen = waitForContextMenu(dbg);
  info(`Open ${elementName} context menu on line ${line || ""}`);
  rightClickElement(dbg, elementName, line);
  return waitForOpen;
}

/**
 * Select a range of lines in the editor and open the contextmenu
 * @param {Object} dbg
 * @param {Object} lines
 * @param {String} elementName
 * @returns
 */

async function selectEditorLinesAndOpenContextMenu(
  dbg,
  lines,
  elementName = "line"
) {
  const { startLine, endLine } = lines;
  setSelection(dbg, startLine, endLine ?? startLine);
  return openContextMenuInDebugger(dbg, elementName, startLine);
}

/**
 * Asserts that the styling for ignored lines are applied
 * @param {Object} dbg
 * @param {Object} options
 *                 lines {null | Number[]} [lines] Line(s) to assert.
 *                   - If null is passed, the assertion is on all the blackboxed lines
 *                   - If an array of one item (start line) is passed, the assertion is on the specified line
 *                   - If an array (start and end lines) is passed, the assertion is on the multiple lines seelected
 *                 hasBlackboxedLinesClass
 *                   If `true` assert that style exist, else assert that style does not exist
 */

async function assertIgnoredStyleInSourceLines(
  dbg,
  { lines, hasBlackboxedLinesClass }
) {
  if (lines) {
    let currentLine = lines[0];
    do {
      const element = await getNodeAtEditorLine(dbg, currentLine);
      const hasStyle = element.classList.contains("blackboxed-line");
      is(
        hasStyle,
        hasBlackboxedLinesClass,
        `Line ${currentLine} ${
          hasBlackboxedLinesClass ? "does not have" : "has"
        } ignored styling`
      );
      currentLine = currentLine + 1;
    } while (currentLine <= lines[1]);
  } else {
    const codeLines = findAllElements(dbg, "codeLines");
    const blackboxedLines = findAllElements(dbg, "blackboxedLines");
    is(
      hasBlackboxedLinesClass ? codeLines.length : 0,
      blackboxedLines.length,
      `${blackboxedLines.length} of ${codeLines.length} lines are blackboxed`
    );
  }
}

/**
 * Assert the text content on the line matches what is
 * expected.
 *
 * @param {Object} dbg
 * @param {Number} line
 * @param {String} expectedTextContent
 */

function assertTextContentOnLine(dbg, line, expectedTextContent) {
  const lineInfo = getCMEditor(dbg).lineInfo(isCm6Enabled ? line : line - 1);
  const textContent = lineInfo.text.trim();
  is(textContent, expectedTextContent, `Expected text content on line ${line}`);
}

/*
 * Assert that no breakpoint is set on a given line of
 * the currently selected source in the editor.
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {Number} line Line where to check for a breakpoint in the editor
 * @static
 */

async function assertNoBreakpoint(dbg, line) {
  const el = await getNodeAtEditorGutterLine(dbg, line);

  const exists = el.classList.contains(
    isCm6Enabled ? "cm6-gutter-breakpoint" : "new-breakpioint"
  );
  ok(!exists, `Breakpoint doesn't exists on line ${line}`);
}

/*
 * Assert that a regular breakpoint is set in the currently
 * selected source in the editor. (no conditional, nor log breakpoint)
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {Number} line Line where to check for a breakpoint
 * @static
 */

async function assertBreakpoint(dbg, line) {
  let el = await getNodeAtEditorGutterLine(dbg, line);
  el = isCm6Enabled ? el.firstChild : el;

  ok(
    el.classList.contains(selectors.gutterBreakpoint),
    `Breakpoint exists on line ${line}`
  );

  const hasConditionClass = el.classList.contains("has-condition");
  ok(
    !hasConditionClass,
    `Regular breakpoint doesn't have condition on line ${line}`
  );

  const hasLogClass = el.classList.contains("has-log");
  ok(!hasLogClass, `Regular breakpoint doesn't have log on line ${line}`);
}

/*
 * Assert that a conditionnal breakpoint is set.
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {Number} line Line where to check for a breakpoint
 * @static
 */

async function assertConditionBreakpoint(dbg, line) {
  let el = await getNodeAtEditorGutterLine(dbg, line);
  el = isCm6Enabled ? el.firstChild : el;

  ok(
    el.classList.contains(selectors.gutterBreakpoint),
    `Breakpoint exists on line ${line}`
  );

  const hasConditionClass = el.classList.contains("has-condition");
  ok(hasConditionClass, `Conditional breakpoint on line ${line}`);

  const hasLogClass = el.classList.contains("has-log");
  ok(
    !hasLogClass,
    `Conditional breakpoint doesn't have log breakpoint on line ${line}`
  );
}

/*
 * Assert that a log breakpoint is set.
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {Number} line Line where to check for a breakpoint
 * @static
 */

async function assertLogBreakpoint(dbg, line) {
  let el = await getNodeAtEditorGutterLine(dbg, line);
  el = isCm6Enabled ? el.firstChild : el;

  ok(
    el.classList.contains(selectors.gutterBreakpoint),
    `Breakpoint exists on line ${line}`
  );

  const hasConditionClass = el.classList.contains("has-condition");
  ok(
    !hasConditionClass,
    `Log breakpoint doesn't have condition on line ${line}`
  );

  const hasLogClass = el.classList.contains("has-log");
  ok(hasLogClass, `Log breakpoint on line ${line}`);
}

function assertBreakpointSnippet(dbg, index, expectedSnippet) {
  const actualSnippet = findElement(dbg, "breakpointLabel", 2).innerText;
  is(actualSnippet, expectedSnippet, `Breakpoint ${index} snippet`);
}

const selectors = {
  callStackBody: ".call-stack-pane .pane",
  domMutationItem: ".dom-mutation-list li",
  expressionNode: i =>
    `.expressions-list .expression-container:nth-child(${i}) .object-label`,
  expressionValue: i =>
    // eslint-disable-next-line max-len
    `.expressions-list .expression-container:nth-child(${i}) .object-delimiter + *`,
  expressionInput: ".watch-expressions-pane input.input-expression",
  expressionNodes: ".expressions-list .tree-node",
  expressionPlus: ".watch-expressions-pane button.plus",
  expressionRefresh: ".watch-expressions-pane button.refresh",
  expressionsHeader: ".watch-expressions-pane ._header .header-label",
  scopesHeader: ".scopes-pane ._header .header-label",
  breakpointItem: i => `.breakpoints-list div:nth-of-type(${i})`,
  breakpointLabel: i => `${selectors.breakpointItem(i)} .breakpoint-label`,
  breakpointHeadings: ".breakpoints-list .breakpoint-heading",
  breakpointItems: ".breakpoints-list .breakpoint",
  breakpointContextMenu: {
    disableSelf: "#node-menu-disable-self",
    disableAll: "#node-menu-disable-all",
    disableOthers: "#node-menu-disable-others",
    enableSelf: "#node-menu-enable-self",
    enableOthers: "#node-menu-enable-others",
    disableDbgStatement: "#node-menu-disable-dbgStatement",
    enableDbgStatement: "#node-menu-enable-dbgStatement",
    remove: "#node-menu-delete-self",
    removeOthers: "#node-menu-delete-other",
    removeCondition: "#node-menu-remove-condition",
  },
  blackboxedLines: isCm6Enabled
    ? ".cm-content > .blackboxed-line"
    : ".CodeMirror-code .blackboxed-line",
  codeLines: isCm6Enabled
    ? ".cm-content > .cm-line"
    : ".CodeMirror-code .CodeMirror-line",
  editorContextMenu: {
    continueToHere: "#node-menu-continue-to-here",
  },
  columnBreakpoints: ".column-breakpoint",
  scopes: ".scopes-list",
  scopeNodes: ".scopes-list .object-label",
  scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
  scopeValue: i =>
    `.scopes-list .tree-node:nth-child(${i}) .object-delimiter + *`,
  mapScopesCheckbox: ".map-scopes-header input",
  asyncframe: i =>
    `.frames div[role=listbox] .location-async-cause:nth-child(${i})`,
  frame: i => `.frames div[role=listbox] .frame:nth-child(${i})`,
  frames: ".frames [role='listbox'] .frame",
  gutterBreakpoint: isCm6Enabled ? "breakpoint-marker" : "new-breakpoint",
  // This is used to trigger events (click etc) on the gutter
  gutterElement: i =>
    isCm6Enabled
      ? `.cm-gutter.cm-lineNumbers .cm-gutterElement:nth-child(${i + 1})`
      : `.CodeMirror-code *:nth-child(${i}) .CodeMirror-linenumber`,
  gutters: isCm6Enabled ? `.cm-gutters` : `.CodeMirror-gutters`,
  line: i =>
    isCm6Enabled
      ? `.cm-content > div.cm-line:nth-child(${i})`
      : `.CodeMirror-code div:nth-child(${i}) .CodeMirror-line`,
  addConditionItem:
    "#node-menu-add-condition, #node-menu-add-conditional-breakpoint",
  editConditionItem:
    "#node-menu-edit-condition, #node-menu-edit-conditional-breakpoint",
  addLogItem: "#node-menu-add-log-point",
  editLogItem: "#node-menu-edit-log-point",
  disableItem: "#node-menu-disable-breakpoint",
  breakpoint: isCm6Enabled
    ? ".cm-gutter > .cm6-gutter-breakpoint"
    : ".CodeMirror-code > .new-breakpoint",
  highlightLine: isCm6Enabled
    ? ".cm-content > .highlight-line"
    : ".CodeMirror-code > .highlight-line",
  debugLine: ".new-debug-line",
  debugErrorLine: ".new-debug-line-error",
  codeMirror: isCm6Enabled ? ".cm-editor" : ".CodeMirror",
  resume: ".resume.active",
  pause: ".pause.active",
  sourceTabs: ".source-tabs",
  activeTab: ".source-tab.active",
  stepOver: ".stepOver.active",
  stepOut: ".stepOut.active",
  stepIn: ".stepIn.active",
  prettyPrintButton: ".source-footer .prettyPrint",
  mappedSourceLink: ".source-footer .mapped-source",
  sourceMapFooterButton: ".debugger-source-map-button",
  sourceNode: i => `.sources-list .tree-node:nth-child(${i}) .node`,
  sourceNodes: ".sources-list .tree-node",
  sourceTreeThreads: '.sources-list .tree-node[aria-level="1"]',
  sourceTreeGroups: '.sources-list .tree-node[aria-level="2"]',
  sourceTreeFiles: ".sources-list .tree-node[data-expandable=false]",
  threadSourceTree: i => `.threads-list .sources-pane:nth-child(${i})`,
  sourceDirectoryLabel: i => `.sources-list .tree-node:nth-child(${i}) .label`,
  resultItems: ".result-list .result-item",
  resultItemName: (name, i) =>
    `${selectors.resultItems}:nth-child(${i})[title$="${name}"]`,
  fileMatch: ".project-text-search .line-value",
  popup: ".popover",
  previewPopup: ".preview-popup",
  openInspector: "button.open-inspector",
  outlineItem: i =>
    `.outline-list__element:nth-child(${i}) .function-signature`,
  outlineItems: ".outline-list__element",
  conditionalPanel: ".conditional-breakpoint-panel",
  conditionalPanelInput: `.conditional-breakpoint-panel  ${
    isCm6Enabled ? ".cm-content" : "textarea"
  }`,
  logPanelInput: `.conditional-breakpoint-panel.log-point ${
    isCm6Enabled ? ".cm-content" : "textarea"
  }`,
  conditionalBreakpointInSecPane: ".breakpoint.is-conditional",
  logPointPanel: ".conditional-breakpoint-panel.log-point",
  logPointInSecPane: ".breakpoint.is-log",
  searchField: ".search-field",
  blackbox: ".action.black-box",
  projectSearchSearchInput: ".project-text-search .search-field input",
  projectSearchCollapsed: ".project-text-search .arrow:not(.expanded)",
  projectSearchExpandedResults: ".project-text-search .result",
  projectSearchFileResults: ".project-text-search .file-result",
  projectSearchModifiersCaseSensitive:
    ".project-text-search button.case-sensitive-btn",
  projectSearchModifiersRegexMatch:
    ".project-text-search button.regex-match-btn",
  projectSearchModifiersWholeWordMatch:
    ".project-text-search button.whole-word-btn",
  projectSearchRefreshButton: ".project-text-search button.refresh-btn",
  threadsPaneItems: ".threads-pane .thread",
  threadsPaneItem: i => `.threads-pane .thread:nth-child(${i})`,
  threadsPaneItemPause: i => `${selectors.threadsPaneItem(i)}.paused`,
  CodeMirrorLines: isCm6Enabled ? ".cm-content" : ".CodeMirror-lines",
  CodeMirrorCode: isCm6Enabled ? ".cm-content" : ".CodeMirror-code",
  inlinePreview: isCm6Enabled
    ? ".cm-content .inline-preview"
    : ".CodeMirror-code .CodeMirror-widget",
  inlinePreviewLabels: ".inline-preview .inline-preview-label",
  inlinePreviewValues: ".inline-preview .inline-preview-value",
  inlinePreviewOpenInspector: ".inline-preview-value button.open-inspector",
  watchpointsSubmenu: "#node-menu-watchpoints",
  addGetWatchpoint: "#node-menu-add-get-watchpoint",
  logEventsCheckbox: ".events-header input",
  previewPopupInvokeGetterButton: ".preview-popup .invoke-getter",
  previewPopupObjectNumber: ".preview-popup .objectBox-number",
  previewPopupObjectObject: ".preview-popup .objectBox-object",
  sourceTreeRootNode: ".sources-panel .node .window",
  sourceTreeFolderNode: ".sources-panel .node .folder",
  excludePatternsInput: ".project-text-search .exclude-patterns-field input",
  fileSearchInput: ".search-bar input",
  watchExpressionsHeader: ".watch-expressions-pane ._header .header-label",
  watchExpressionsAddButton: ".watch-expressions-pane ._header .plus",
  editorNotificationFooter: ".editor-notification-footer",
};

function getSelector(elementName, ...args) {
  let selector = selectors[elementName];
  if (!selector) {
    throw new Error(`The selector ${elementName} is not defined`);
  }

  if (typeof selector == "function") {
    selector = selector(...args);
  }

  return selector;
}

function findElement(dbg, elementName, ...args) {
  const selector = getSelector(elementName, ...args);
  return findElementWithSelector(dbg, selector);
}

function findElementWithSelector(dbg, selector) {
  return dbg.win.document.querySelector(selector);
}

function findAllElements(dbg, elementName, ...args) {
  const selector = getSelector(elementName, ...args);
  return findAllElementsWithSelector(dbg, selector);
}

function findAllElementsWithSelector(dbg, selector) {
  return dbg.win.document.querySelectorAll(selector);
}

function getSourceNodeLabel(dbg, index) {
  return findElement(dbg, "sourceNode", index)
    .textContent.trim()
    .replace(/^[\s\u200b]*/g, "");
}

/**
 * Simulates a mouse click in the debugger DOM.
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {String} elementName
 * @param {Array} args
 * @return {Promise}
 * @static
 */

async function clickElement(dbg, elementName, ...args) {
  const selector = getSelector(elementName, ...args);
  const el = await waitForElementWithSelector(dbg, selector);

  el.scrollIntoView();

  return clickElementWithSelector(dbg, selector);
}

function clickElementWithSelector(dbg, selector) {
  clickDOMElement(dbg, findElementWithSelector(dbg, selector));
}

function clickDOMElement(dbg, element, options = {}) {
  EventUtils.synthesizeMouseAtCenter(element, options, dbg.win);
}

function dblClickElement(dbg, elementName, ...args) {
  const selector = getSelector(elementName, ...args);

  return EventUtils.synthesizeMouseAtCenter(
    findElementWithSelector(dbg, selector),
    { clickCount: 2 },
    dbg.win
  );
}

function clickElementWithOptions(dbg, elementName, options, ...args) {
  const selector = getSelector(elementName, ...args);
  const el = findElementWithSelector(dbg, selector);
  el.scrollIntoView();

  return EventUtils.synthesizeMouseAtCenter(el, options, dbg.win);
}

function altClickElement(dbg, elementName, ...args) {
  return clickElementWithOptions(dbg, elementName, { altKey: true }, ...args);
}

function shiftClickElement(dbg, elementName, ...args) {
  return clickElementWithOptions(dbg, elementName, { shiftKey: true }, ...args);
}

function rightClickElement(dbg, elementName, ...args) {
  const selector = getSelector(elementName, ...args);
  return rightClickEl(dbg, dbg.win.document.querySelector(selector));
}

function rightClickEl(dbg, el) {
  el.scrollIntoView();
  EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win);
}

async function clearElement(dbg, elementName) {
  await clickElement(dbg, elementName);
  await pressKey(dbg, "End");
  const selector = getSelector(elementName);
  const el = findElementWithSelector(dbg, getSelector(elementName));
  let len = el.value.length;
  while (len) {
    pressKey(dbg, "Backspace");
    len--;
  }
}

async function clickGutter(dbg, line) {
  const el = await (isCm6Enabled
    ? scrollAndGetEditorLineGutterElement(dbg, line)
    : codeMirrorGutterElement(dbg, line));
  clickDOMElement(dbg, el);
}

async function cmdClickGutter(dbg, line) {
  const el = await (isCm6Enabled
    ? scrollAndGetEditorLineGutterElement(dbg, line)
    : codeMirrorGutterElement(dbg, line));
  clickDOMElement(dbg, el, cmdOrCtrl);
}

function findContextMenu(dbg, selector) {
  // the context menu is in the toolbox window
  const doc = dbg.toolbox.topDoc;

  // there are several context menus, we want the one with the menu-api
  const popup = doc.querySelector('menupopup[menu-api="true"]');

  return popup.querySelector(selector);
}

// Waits for the context menu to exist and to fully open. Once this function
// completes, selectContextMenuItem can be called.
// waitForContextMenu must be called after menu opening has been triggered, e.g.
// after synthesizing a right click / contextmenu event.
async function waitForContextMenu(dbg) {
  // the context menu is in the toolbox window
  const doc = dbg.toolbox.topDoc;

  // there are several context menus, we want the one with the menu-api
  const popup = await waitFor(() =>
    doc.querySelector('menupopup[menu-api="true"]')
  );

  if (popup.state == "open") {
    return popup;
  }

  await new Promise(resolve => {
    popup.addEventListener("popupshown", () => resolve(), { once: true });
  });

  return popup;
}

/**
 * Closes and open context menu popup.
 *
 * @memberof mochitest/helpers
 * @param {Object} dbg
 * @param {String} popup - The currently opened popup returned by
 *                         `waitForContextMenu`.
 * @return {Promise}
 */


async function closeContextMenu(dbg, popup) {
  const onHidden = new Promise(resolve => {
    popup.addEventListener("popuphidden", resolve, { once: true });
  });
  popup.hidePopup();
  return onHidden;
}

function selectContextMenuItem(dbg, selector) {
  const item = findContextMenu(dbg, selector);
  item.closest("menupopup").activateItem(item);
}

async function openContextMenuSubmenu(dbg, selector) {
  const item = findContextMenu(dbg, selector);
  const popup = item.menupopup;
  const popupshown = new Promise(resolve => {
    popup.addEventListener("popupshown", () => resolve(), { once: true });
  });
  item.openMenu(true);
  await popupshown;
  return popup;
}

async function assertContextMenuLabel(dbg, selector, expectedLabel) {
  const item = await waitFor(() => findContextMenu(dbg, selector));
  is(
    item.label,
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=86 H=92 G=88

¤ Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.0.79Bemerkung:  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.