summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/test/mochitest/head.js
blob: 6130e02133a32ea59debdac0dde57333ec5ecd7d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/* 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/>. */

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

"use strict";

// This head.js file is only imported by debugger mochitests.
// Anything that is meant to be used by tests of other panels should be moved to shared-head.js
// Also, any symbol that may conflict with other test symbols should stay in head.js
// (like EXAMPLE_URL)

const EXAMPLE_URL =
  "https://example.com/browser/devtools/client/debugger/test/mochitest/examples/";

// This URL is remote compared to EXAMPLE_URL, as one uses .com and the other uses .org
// Note that this depends on initDebugger to always use EXAMPLE_URL
const EXAMPLE_REMOTE_URL =
  "https://example.org/browser/devtools/client/debugger/test/mochitest/examples/";

const EXAMPLE_URL_WITH_PORT =
  "http://mochi.test:8888/browser/devtools/client/debugger/test/mochitest/examples/";

// shared-head.js handles imports, constants, and utility functions
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
  this
);

Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js",
  this
);

Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
  this
);

// Clear preferences that may be set during the course of tests.
registerCleanupFunction(() => {
  info("finish() was called, cleaning up and clearing debugger preferences...");
  Services.prefs.clearUserPref("devtools.debugger.map-scopes-enabled");
});

/**
 * Helper function for `_loadAllIntegrationTests`.
 *
 * Implements this as a global method in order to please eslint.
 * This will be used by modules loaded from "integration-tests" folder
 * in order to register a new integration task, ran when executing `runAllIntegrationTests`.
 */
const integrationTasks = [];
function addIntegrationTask(fun) {
  integrationTasks.push(fun);
}

/**
 * Helper function for `runAllIntegrationTests`.
 *
 * Loads all the modules from "integration-tests" folder and return
 * all the task they implemented and registered by calling `addIntegrationTask`.
 */
function _loadAllIntegrationTests() {
  const testsDir = getChromeDir(getResolvedURI(gTestPath));
  testsDir.append("integration-tests");
  const entries = testsDir.directoryEntries;
  const urls = [];
  while (entries.hasMoreElements()) {
    const file = entries.nextFile;
    const url = Services.io.newFileURI(file).spec;
    if (url.endsWith(".js")) {
      urls.push(url);
    }
  }

  // We need to sort in order to run the test in a reliable order
  urls.sort();

  for (const url of urls) {
    Services.scriptloader.loadSubScript(url, this);
  }
  return integrationTasks;
}

/**
 * Method to be called by each integration tests which will
 * run all the "integration tasks" implemented in files from the "integration-tests" folder.
 * These files should call the `addIntegrationTask()` method to register something to run.
 *
 * @param {String} testFolder
 *        Define what folder in "examples" folder to load before opening the debugger.
 *        This is meant to be a versionized test folder with v1, v2, v3 folders.
 *        (See createVersionizedHttpTestServer())
 * @param {Object} env
 *        Environment object passed down to each task to better know
 *        which particular integration test is being run.
 */
async function runAllIntegrationTests(testFolder, env) {
  const tasks = _loadAllIntegrationTests();

  const testServer = createVersionizedHttpTestServer("examples/" + testFolder);
  const testUrl = testServer.urlFor("index.html");

  for (const task of tasks) {
    info(` ==> Running integration task '${task.name}'`);
    await task(testServer, testUrl, env);
  }
}

const INTEGRATION_TEST_PAGE_SOURCES = [
  "index.html",
  "iframe.html",
  "script.js",
  "onload.js",
  "test-functions.js",
  "query.js?x=1",
  "query.js?x=2",
  "query2.js?y=3",
  "bundle.js",
  "original.js",
  "bundle-with-another-original.js",
  "original-with-no-update.js",
  "replaced-bundle.js",
  "removed-original.js",
  "named-eval.js",
  "react-component-module.js",
  // This is the JS file with encoded characters and custom protocol
  "文字コード.js",
  // Webpack generated some extra sources:
  "bootstrap 3b1a221408fdde86aa49",
  "bootstrap a1ecee2f86e1d0ea3fb5",
  "bootstrap d343aa81956b90d9f67e",
  // There is 3 occurences, one per target (main thread, worker and iframe).
  // But there is even more source actors (named evals and duplicated script tags).
  "same-url.sjs",
  "same-url.sjs",
];
// The iframe one is only available when fission is enabled, or EFT
if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
  INTEGRATION_TEST_PAGE_SOURCES.push("same-url.sjs");
}

/**
 * Install a Web Extension which will run a content script against any test page
 * served from https://example.com
 *
 * This content script is meant to be debuggable when devtools.chrome.enabled is true.
 */
async function installAndStartContentScriptExtension() {
  function contentScript() {
    console.log("content script loads");

    // This listener prevents the source from being garbage collected
    // and be missing from the scripts returned by `dbg.findScripts()`
    // in `ThreadActor._discoverSources`.
    window.onload = () => {};
  }

  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      name: "Test content script extension",
      content_scripts: [
        {
          js: ["content_script.js"],
          matches: ["https://example.com/*"],
          run_at: "document_start",
        },
      ],
    },
    files: {
      "content_script.js": contentScript,
    },
  });

  await extension.startup();

  return extension;
}

/**
 * Return the text content for a given line in the Source Tree.
 *
 * @param {Object} dbg
 * @param {Number} index
 *        Line number in the source tree
 */
function getSourceTreeLabel(dbg, index) {
  return (
    findElement(dbg, "sourceNode", index)
      .textContent.trim()
      // There is some special whitespace character which aren't removed by trim()
      .replace(/^[\s\u200b]*/g, "")
  );
}

/**
 * Find and assert the source tree node with the specified text
 * exists on the source tree.
 *
 * @param {Object} dbg
 * @param {String} text The node text displayed
 */
async function assertSourceTreeNode(dbg, text) {
  let node = null;
  await waitUntil(() => {
    node = findSourceNodeWithText(dbg, text);
    return !!node;
  });
  ok(!!node, `Source tree node with text "${text}" exists`);
}

/**
 * Assert precisely the list of all breakable line for a given source
 *
 * @param {Object} dbg
 * @param {Object|String} file
 *        The source name or source object to review
 * @param {Number} numberOfLines
 *        The expected number of lines for this source.
 * @param {Array<Number>} breakableLines
 *        This list of all breakable line numbers
 */
async function assertBreakableLines(
  dbg,
  source,
  numberOfLines,
  breakableLines
) {
  await selectSource(dbg, source);
  is(
    getCM(dbg).lineCount(),
    numberOfLines,
    `We show the expected number of lines in CodeMirror for ${source}`
  );
  for (let line = 1; line <= numberOfLines; line++) {
    assertLineIsBreakable(dbg, source, line, breakableLines.includes(line));
  }
}

/**
 * Helper alongside assertBreakable lines to ease defining list of breakable lines.
 *
 * @param {Number} start
 * @param {Number} end
 * @return {Array<Number>}
 *         Returns an array of decimal numbers starting from `start` and ending with `end`.
 */
function getRange(start, end) {
  const range = [];
  for (let i = start; i <= end; i++) {
    range.push(i);
  }
  return range;
}

/**
 * Wait for CodeMirror to start searching
 */
function waitForSearchState(dbg) {
  const cm = getCM(dbg);
  return waitFor(() => cm.state.search);
}

/**
 * Get the currently selected line number displayed in the editor's footer.
 */
function assertCursorPosition(dbg, expectedLine, expectedColumn, message) {
  const cursorPosition = findElementWithSelector(dbg, ".cursor-position");
  if (!cursorPosition) {
    ok(false, message + " (no cursor displayed)");
  }
  // Cursor position text has the following shape: (L, C)
  // where L is the line number, and C the column number
  const match = cursorPosition.innerText.match(/\((\d+), (\d+)\)/);
  if (!match) {
    ok(
      false,
      message + ` (wrong cursor content : '${cursorPosition.innerText}')`
    );
  }
  const [_, line, column] = match;
  is(parseInt(line, 10), expectedLine, message + " (line)");
  is(parseInt(column, 10), expectedColumn, message + " (column)");
}

async function waitForCursorPosition(dbg, expectedLine) {
  return waitFor(() => {
    const cursorPosition = findElementWithSelector(dbg, ".cursor-position");
    if (!cursorPosition) {
      return false;
    }
    const { innerText } = cursorPosition;
    // Cursor position text has the following shape: (L, C)
    // where L is the line number, and C the column number
    const line = innerText.substring(1, innerText.indexOf(","));
    return parseInt(line, 10) == expectedLine;
  });
}