diff options
Diffstat (limited to 'devtools/client/debugger/test/mochitest/browser_dbg-features-source-tree.js')
-rw-r--r-- | devtools/client/debugger/test/mochitest/browser_dbg-features-source-tree.js | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-features-source-tree.js b/devtools/client/debugger/test/mochitest/browser_dbg-features-source-tree.js new file mode 100644 index 0000000000..320e157b70 --- /dev/null +++ b/devtools/client/debugger/test/mochitest/browser_dbg-features-source-tree.js @@ -0,0 +1,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); + const nsiFile = FileUtils.getFile("TmpD", [ + `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}`); +} |