summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/test/mochitest/browser_dbg-features-source-tree.js
blob: 4590f0c2b32f871581f1dc55740d1d573c41c460 (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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
/* 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 test focuses on the SourceTree component, where we display all debuggable sources.
 *
 * The first two tests expand the tree via manual DOM events (first with clicks and second with keys).
 * `waitForSourcesInSourceTree()` is a key assertion method. Passing `{noExpand: true}`
 * is important to avoid automatically expand the source tree.
 *
 * The following tests depend on auto-expand and only assert all the sources possibly displayed
 */

"use strict";

const testServer = createVersionizedHttpTestServer(
  "examples/sourcemaps-reload-uncompressed"
);
const TEST_URL = testServer.urlFor("index.html");

/**
 * This test opens the SourceTree manually via click events on the nested source,
 * and then adds a source dynamically and asserts it is visible.
 */
add_task(async function testSimpleSourcesWithManualClickExpand() {
  const dbg = await initDebugger(
    "doc-sources.html",
    "simple1.js",
    "simple2.js",
    "nested-source.js",
    "long.js"
  );

  // Expand nodes and make sure more sources appear.
  is(
    getSourceTreeLabel(dbg, 1),
    "Main Thread",
    "Main thread is labeled properly"
  );
  info("Before interacting with the source tree, no source are displayed");
  await waitForSourcesInSourceTree(dbg, [], { noExpand: true });
  await clickElement(dbg, "sourceDirectoryLabel", 3);
  info(
    "After clicking on the directory, all sources but the nested ones are displayed"
  );
  await waitForSourcesInSourceTree(
    dbg,
    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
    { noExpand: true }
  );

  await clickElement(dbg, "sourceDirectoryLabel", 4);
  info(
    "After clicking on the nested directory, the nested source is also displayed"
  );
  await waitForSourcesInSourceTree(
    dbg,
    [
      "doc-sources.html",
      "simple1.js",
      "simple2.js",
      "long.js",
      "nested-source.js",
    ],
    { noExpand: true }
  );

  const selected = waitForDispatch(dbg.store, "SET_SELECTED_LOCATION");
  await clickElement(dbg, "sourceNode", 5);
  await selected;
  await waitForSelectedSource(dbg, "nested-source.js");

  // Ensure the source file clicked is now focused
  await waitForElementWithSelector(dbg, ".sources-list .focused");

  const selectedSource = dbg.selectors.getSelectedSource().url;
  ok(selectedSource.includes("nested-source.js"), "nested-source is selected");
  await assertNodeIsFocused(dbg, 5);

  // Make sure new sources appear in the list.
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
    const script = content.document.createElement("script");
    script.src = "math.min.js";
    content.document.body.appendChild(script);
  });

  info("After adding math.min.js, we got a new source displayed");
  await waitForSourcesInSourceTree(
    dbg,
    [
      "doc-sources.html",
      "simple1.js",
      "simple2.js",
      "long.js",
      "nested-source.js",
      "math.min.js",
    ],
    { noExpand: true }
  );
  is(
    getSourceNodeLabel(dbg, 8),
    "math.min.js",
    "math.min.js - The dynamic script exists"
  );

  info("Assert that nested-source.js is still the selected source");
  await assertNodeIsFocused(dbg, 5);

  info("Test the copy to clipboard context menu");
  const mathMinTreeNode = findSourceNodeWithText(dbg, "math.min.js");
  await triggerSourceTreeContextMenu(
    dbg,
    mathMinTreeNode,
    "#node-menu-copy-source"
  );
  const clipboardData = SpecialPowers.getClipboardData("text/plain");
  is(
    clipboardData,
    EXAMPLE_URL + "math.min.js",
    "The clipboard content is the selected source URL"
  );

  info("Test the download file context menu");
  // Before trigerring the menu, mock the file picker
  const MockFilePicker = SpecialPowers.MockFilePicker;
  MockFilePicker.init(window.browsingContext);
  const nsiFile = new FileUtils.File(
    PathUtils.join(PathUtils.tempDir, `export_source_content_${Date.now()}.log`)
  );
  MockFilePicker.setFiles([nsiFile]);
  const path = nsiFile.path;

  await triggerSourceTreeContextMenu(
    dbg,
    mathMinTreeNode,
    "#node-menu-download-file"
  );

  info("Wait for the downloaded file to be fully saved to disk");
  await BrowserTestUtils.waitForCondition(() => IOUtils.exists(path));
  await BrowserTestUtils.waitForCondition(async () => {
    const { size } = await IOUtils.stat(path);
    return size > 0;
  });
  const buffer = await IOUtils.read(path);
  const savedFileContent = new TextDecoder().decode(buffer);

  const mathMinRequest = await fetch(EXAMPLE_URL + "math.min.js");
  const mathMinContent = await mathMinRequest.text();

  is(
    savedFileContent,
    mathMinContent,
    "The downloaded file has the expected content"
  );

  dbg.toolbox.closeToolbox();
});

/**
 * Test keyboard arrow behaviour on the SourceTree with a nested folder
 * that we manually expand/collapse via arrow keys.
 */
add_task(async function testSimpleSourcesWithManualKeyShortcutsExpand() {
  const dbg = await initDebugger(
    "doc-sources.html",
    "simple1.js",
    "simple2.js",
    "nested-source.js",
    "long.js"
  );

  // Before clicking on the source label, no source is displayed
  await waitForSourcesInSourceTree(dbg, [], { noExpand: true });
  await clickElement(dbg, "sourceDirectoryLabel", 3);
  // Right after, all sources, but the nested one are displayed
  await waitForSourcesInSourceTree(
    dbg,
    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
    { noExpand: true }
  );

  // Right key on open dir
  await pressKey(dbg, "Right");
  await assertNodeIsFocused(dbg, 3);

  // Right key on closed dir
  await pressKey(dbg, "Right");
  await assertNodeIsFocused(dbg, 4);

  // Left key on a open dir
  await pressKey(dbg, "Left");
  await assertNodeIsFocused(dbg, 4);

  // Down key on a closed dir
  await pressKey(dbg, "Down");
  await assertNodeIsFocused(dbg, 4);

  // Right key on a source
  // We are focused on the nested source and up to this point we still display only the 4 initial sources
  await waitForSourcesInSourceTree(
    dbg,
    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
    { noExpand: true }
  );
  await pressKey(dbg, "Right");
  await assertNodeIsFocused(dbg, 4);
  // Now, the nested source is also displayed
  await waitForSourcesInSourceTree(
    dbg,
    [
      "doc-sources.html",
      "simple1.js",
      "simple2.js",
      "long.js",
      "nested-source.js",
    ],
    { noExpand: true }
  );

  // Down key on a source
  await pressKey(dbg, "Down");
  await assertNodeIsFocused(dbg, 5);

  // Go to bottom of tree and press down key
  await pressKey(dbg, "Down");
  await pressKey(dbg, "Down");
  await assertNodeIsFocused(dbg, 6);

  // Up key on a source
  await pressKey(dbg, "Up");
  await assertNodeIsFocused(dbg, 5);

  // Left key on a source
  await pressKey(dbg, "Left");
  await assertNodeIsFocused(dbg, 4);

  // Left key on a closed dir
  // We are about to close the nested folder, the nested source is about to disappear
  await waitForSourcesInSourceTree(
    dbg,
    [
      "doc-sources.html",
      "simple1.js",
      "simple2.js",
      "long.js",
      "nested-source.js",
    ],
    { noExpand: true }
  );
  await pressKey(dbg, "Left");
  // And it disappeared
  await waitForSourcesInSourceTree(
    dbg,
    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
    { noExpand: true }
  );
  await pressKey(dbg, "Left");
  await assertNodeIsFocused(dbg, 3);

  // Up Key at the top of the source tree
  await pressKey(dbg, "Up");
  await assertNodeIsFocused(dbg, 2);
  dbg.toolbox.closeToolbox();
});

/**
 * Tests that the source tree works with all the various types of sources
 * coming from the integration test page.
 *
 * Also assert a few extra things on sources with query strings:
 *  - they can be pretty printed,
 *  - quick open matches them,
 *  - you can set breakpoint on them.
 */
add_task(async function testSourceTreeOnTheIntegrationTestPage() {
  // We open against a blank page and only then navigate to the test page
  // so that sources aren't GC-ed before opening the debugger.
  // When we (re)load a page while the debugger is opened, the debugger
  // will force all sources to be held in memory.
  const dbg = await initDebuggerWithAbsoluteURL("about:blank");

  await navigateToAbsoluteURL(
    dbg,
    TEST_URL,
    "index.html",
    "script.js",
    "test-functions.js",
    "query.js?x=1",
    "query.js?x=2",
    "query2.js?y=3",
    "bundle.js",
    "original.js",
    "replaced-bundle.js",
    "removed-original.js",
    "named-eval.js"
  );

  info("Verify source tree content");
  await waitForSourcesInSourceTree(dbg, INTEGRATION_TEST_PAGE_SOURCES);

  info("Verify Thread Source Items");
  const mainThreadItem = findSourceTreeThreadByName(dbg, "Main Thread");
  ok(mainThreadItem, "Found the thread item for the main thread");
  ok(
    mainThreadItem.querySelector("span.img.window"),
    "The thread has the window icon"
  );

  info(
    "Assert the number of sources and source actors for the same-url.sjs sources"
  );
  const sameUrlSource = findSource(dbg, "same-url.sjs");
  ok(sameUrlSource, "Found same-url.js in the main thread");

  const sourceActors = dbg.selectors.getSourceActorsForSource(sameUrlSource.id);

  const mainThread = dbg.selectors
    .getAllThreads()
    .find(thread => thread.name == "Main Thread");

  is(
    sourceActors.filter(actor => actor.thread == mainThread.actor).length,
    // When EFT is disabled the iframe's source is meld into the main target
    isEveryFrameTargetEnabled() ? 3 : 4,
    "same-url.js is loaded 3 times in the main thread"
  );

  if (isEveryFrameTargetEnabled()) {
    const iframeThread = dbg.selectors
      .getAllThreads()
      .find(thread => thread.name == testServer.urlFor("iframe.html"));

    is(
      sourceActors.filter(actor => actor.thread == iframeThread.actor).length,
      1,
      "same-url.js is loaded one time in the iframe thread"
    );
  }

  const workerThread = dbg.selectors
    .getAllThreads()
    .find(thread => thread.name == testServer.urlFor("same-url.sjs"));

  is(
    sourceActors.filter(actor => actor.thread == workerThread.actor).length,
    1,
    "same-url.js is loaded one time in the worker thread"
  );

  const workerThreadItem = findSourceTreeThreadByName(dbg, "same-url.sjs");
  ok(workerThreadItem, "Found the thread item for the worker");
  ok(
    workerThreadItem.querySelector("span.img.worker"),
    "The thread has the worker icon"
  );

  info("Verify source icons");
  assertSourceIcon(dbg, "index.html", "file");
  assertSourceIcon(dbg, "script.js", "javascript");
  assertSourceIcon(dbg, "query.js?x=1", "javascript");
  assertSourceIcon(dbg, "original.js", "javascript");
  // Framework icons are only displayed when we parse the source,
  // which happens when we select the source
  assertSourceIcon(dbg, "react-component-module.js", "javascript");
  await selectSource(dbg, "react-component-module.js");
  assertSourceIcon(dbg, "react-component-module.js", "react");

  info("Verify blackbox source icon");
  await selectSource(dbg, "script.js");
  await clickElement(dbg, "blackbox");
  await waitForDispatch(dbg.store, "BLACKBOX_WHOLE_SOURCES");
  assertSourceIcon(dbg, "script.js", "blackBox");
  await clickElement(dbg, "blackbox");
  await waitForDispatch(dbg.store, "UNBLACKBOX_WHOLE_SOURCES");
  assertSourceIcon(dbg, "script.js", "javascript");

  info("Assert the content of the named eval");
  await selectSource(dbg, "named-eval.js");
  assertTextContentOnLine(dbg, 3, `console.log("named-eval");`);

  info("Assert that nameless eval don't show up in the source tree");
  invokeInTab("breakInEval");
  await waitForPaused(dbg);
  await waitForSourcesInSourceTree(dbg, INTEGRATION_TEST_PAGE_SOURCES);
  await resume(dbg);

  info("Assert the content of sources with query string");
  await selectSource(dbg, "query.js?x=1");
  const tab = findElement(dbg, "activeTab");
  is(tab.innerText, "query.js?x=1", "Tab label is query.js?x=1");
  assertTextContentOnLine(
    dbg,
    1,
    `function query() {console.log("query x=1");}`
  );
  await addBreakpoint(dbg, "query.js?x=1", 1);
  assertBreakpointHeading(dbg, "query.js?x=1", 0);

  // pretty print the source and check the tab text
  clickElement(dbg, "prettyPrintButton");
  await waitForSource(dbg, "query.js?x=1:formatted");
  await waitForSelectedSource(dbg, "query.js?x=1:formatted");
  assertSourceIcon(dbg, "query.js?x=1", "prettyPrint");

  const prettyTab = findElement(dbg, "activeTab");
  is(prettyTab.innerText, "query.js?x=1", "Tab label is query.js?x=1");
  ok(prettyTab.querySelector(".img.prettyPrint"));
  assertBreakpointHeading(dbg, "query.js?x=1", 0);
  assertTextContentOnLine(dbg, 1, `function query() {`);
  // Note the replacements of " by ' here:
  assertTextContentOnLine(dbg, 2, `console.log('query x=1');`);

  // assert quick open works with queries
  pressKey(dbg, "quickOpen");
  type(dbg, "query.js?x");

  // There can be intermediate updates in the results,
  // so wait for the final expected value
  await waitFor(async () => {
    const resultItem = findElement(dbg, "resultItems");
    if (!resultItem) {
      return false;
    }
    return resultItem.innerText.includes("query.js?x=1");
  }, "Results include the source with the query string");
  dbg.toolbox.closeToolbox();
});

/**
 * Verify that Web Extension content scripts appear only when
 * devtools.chrome.enabled is set to true and that they get
 * automatically re-selected on page reload.
 */
add_task(async function testSourceTreeWithWebExtensionContentScript() {
  const extension = await installAndStartContentScriptExtension();

  info("Without the chrome preference, the content script doesn't show up");
  await pushPref("devtools.chrome.enabled", false);
  let dbg = await initDebugger("doc-content-script-sources.html");
  // Let some time for unexpected source to appear
  await wait(1000);
  await waitForSourcesInSourceTree(dbg, []);
  await dbg.toolbox.closeToolbox();

  info("With the chrome preference, the content script shows up");
  await pushPref("devtools.chrome.enabled", true);
  const toolbox = await openToolboxForTab(gBrowser.selectedTab, "jsdebugger");
  dbg = createDebuggerContext(toolbox);
  await waitForSourcesInSourceTree(dbg, ["content_script.js"]);
  await selectSource(dbg, "content_script.js");
  ok(
    findElementWithSelector(dbg, ".sources-list .focused"),
    "Source is focused"
  );

  const contentScriptGroupItem = findSourceNodeWithText(
    dbg,
    "Test content script extension"
  );
  ok(contentScriptGroupItem, "Found the group item for the content script");
  ok(
    contentScriptGroupItem.querySelector("span.img.extension"),
    "The group has the extension icon"
  );
  assertSourceIcon(dbg, "content_script.js", "javascript");

  for (let i = 1; i < 3; i++) {
    info(
      `Reloading tab (${i} time), the content script should always be reselected`
    );
    gBrowser.reloadTab(gBrowser.selectedTab);
    await waitForSelectedSource(dbg, "content_script.js");
    ok(
      findElementWithSelector(dbg, ".sources-list .focused"),
      "Source is focused"
    );
  }
  await dbg.toolbox.closeToolbox();

  await extension.unload();
});

add_task(async function testSourceTreeWithEncodedPaths() {
  const httpServer = createTestHTTPServer();
  httpServer.registerContentType("html", "text/html");
  httpServer.registerContentType("js", "application/javascript");

  httpServer.registerPathHandler("/index.html", function (request, response) {
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.write(`<!DOCTYPE html>
    <html>
      <head>
      <script src="/my folder/my file.js"></script>
      <script src="/malformedUri.js?%"></script>
      </head>
      <body>
      <h1>Encoded scripts paths</h1>
      </body>
    `);
  });
  httpServer.registerPathHandler(
    encodeURI("/my folder/my file.js"),
    function (request, response) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/javascript", false);
      response.write(`const x = 42`);
    }
  );
  httpServer.registerPathHandler(
    "/malformedUri.js",
    function (request, response) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/javascript", false);
      response.write(`const y = "malformed"`);
    }
  );
  const port = httpServer.identity.primaryPort;

  const dbg = await initDebuggerWithAbsoluteURL(
    `http://localhost:${port}/index.html`,
    "my file.js"
  );

  await waitForSourcesInSourceTree(dbg, ["my file.js", "malformedUri.js?%"]);
  ok(
    true,
    "source name are decoded in the tree, and malformed uri source are displayed"
  );
  is(
    // We don't have any specific class on the folder item, so let's target the folder
    // icon next sibling, which is the directory label.
    findElementWithSelector(dbg, ".sources-panel .node .folder + .label")
      .innerText,
    "my folder",
    "folder name is decoded in the tree"
  );
});

/**
 * Assert the location displayed in the breakpoint list, in the right sidebar.
 *
 * @param {Object} dbg
 * @param {String} label
 *        The expected displayed location
 * @param {Number} index
 *        The position of the breakpoint in the list to verify
 */
function assertBreakpointHeading(dbg, label, index) {
  const breakpointHeading = findAllElements(dbg, "breakpointHeadings")[index]
    .innerText;
  is(breakpointHeading, label, `Breakpoint heading is ${label}`);
}