/* 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 . */
/**
* This test focus on asserting the source content displayed in CodeMirror
* when we open a source from the SourceTree (or by any other means).
*
* The source content is being fetched from the server only on-demand.
* The main shortcoming is about sources being GC-ed. This only happens
* when we open the debugger on an already loaded page.
* When we (re)load a page while the debugger is opened, sources are never GC-ed.
* There are also specifics related to HTML page having inline scripts.
* Also, as this data is fetched on-demand, there is a loading prompt
* being displayed while the source is being fetched from the server.
*/
"use strict";
const httpServer = createTestHTTPServer();
const BASE_URL = `http://localhost:${httpServer.identity.primaryPort}/`;
const loadCounts = {};
/**
* Simple tests, asserting that we correctly display source text content in CodeMirror
*/
const NAMED_EVAL_CONTENT = `function namedEval() {}; console.log('eval script'); //# sourceURL=named-eval.js`;
const NEW_FUNCTION_CONTENT =
"console.log('new function'); //# sourceURL=new-function.js";
const INDEX_PAGE_CONTENT = `
`;
const IFRAME_CONTENT = `
`;
httpServer.registerPathHandler("/index.html", (request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(INDEX_PAGE_CONTENT);
});
httpServer.registerPathHandler("/normal-script.js", (request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
response.setHeader("Content-Type", "application/javascript");
response.write(`console.log("normal script")`);
});
httpServer.registerPathHandler(
"/slow-loading-script.js",
(request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
response.processAsync();
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
setTimeout(function () {
response.setHeader("Content-Type", "application/javascript");
response.write(`console.log("slow loading script")`);
response.finish();
}, 1000);
}
);
httpServer.registerPathHandler("/http-error-script.js", (request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
response.setStatusLine(request.httpVersion, 404, "Not found");
response.write(`console.log("http error")`);
});
httpServer.registerPathHandler("/same-url.js", (request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
const sameUrlLoadCount = loadCounts[request.path];
// Prevents gecko from cache this request in order to force fetching
// a new, distinct content for each usage of this URL
response.setHeader("Cache-Control", "no-store");
response.setHeader("Content-Type", "application/javascript");
response.write(`console.log("same url #${sameUrlLoadCount}")`);
});
httpServer.registerPathHandler("/iframe.html", (request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
response.setHeader("Content-Type", "text/html");
response.write(IFRAME_CONTENT);
});
add_task(async function testSourceTextContent() {
const dbg = await initDebuggerWithAbsoluteURL("about:blank");
const waitForSources = [
"index.html",
"normal-script.js",
"slow-loading-script.js",
"same-url.js",
"new-function.js",
];
// With fission and EFT disabled, the structure of the source tree changes
// as there is no iframe thread and all the iframe sources are loaded under the
// Main thread, so nodes will be in different positions in the tree.
const noFissionNoEFT = !isFissionEnabled() && !isEveryFrameTargetEnabled();
if (noFissionNoEFT) {
waitForSources.push("iframe.html", "named-eval.js");
}
// Load the document *once* the debugger is opened
// in order to avoid having any source being GC-ed.
await navigateToAbsoluteURL(dbg, BASE_URL + "index.html", ...waitForSources);
await selectSourceFromSourceTree(
dbg,
"new-function.js",
noFissionNoEFT ? 6 : 5,
"Select `new-function.js`"
);
is(
getCM(dbg).getValue(),
`function anonymous(\n) {\n${NEW_FUNCTION_CONTENT}\n}`
);
await selectSourceFromSourceTree(
dbg,
"normal-script.js",
noFissionNoEFT ? 7 : 6,
"Select `normal-script.js`"
);
is(getCM(dbg).getValue(), `console.log("normal script")`);
await selectSourceFromSourceTree(
dbg,
"slow-loading-script.js",
noFissionNoEFT ? 9 : 8,
"Select `slow-loading-script.js`"
);
is(getCM(dbg).getValue(), `console.log("slow loading script")`);
await selectSourceFromSourceTree(
dbg,
"index.html",
noFissionNoEFT ? 4 : 3,
"Select `index.html`"
);
is(getCM(dbg).getValue(), INDEX_PAGE_CONTENT);
await selectSourceFromSourceTree(
dbg,
"named-eval.js",
noFissionNoEFT ? 5 : 4,
"Select `named-eval.js`"
);
is(getCM(dbg).getValue(), NAMED_EVAL_CONTENT);
await selectSourceFromSourceTree(
dbg,
"same-url.js",
noFissionNoEFT ? 8 : 7,
"Select `same-url.js` in the Main Thread"
);
is(
getCM(dbg).getValue(),
`console.log("same url #1")`,
"We get an arbitrary content for same-url, the first loaded one"
);
const sameUrlSource = findSource(dbg, "same-url.js");
const sourceActors = dbg.selectors.getSourceActorsForSource(sameUrlSource.id);
if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
const mainThread = dbg.selectors
.getAllThreads()
.find(thread => thread.name == "Main Thread");
is(
sourceActors.filter(actor => actor.thread == mainThread.actor).length,
3,
"same-url.js is loaded 3 times in the main thread"
);
info(`Close the same-url.js from Main Thread`);
await closeTab(dbg, "same-url.js");
info("Click on the iframe tree node to show sources in the iframe");
await clickElement(dbg, "sourceDirectoryLabel", 9);
await waitForSourcesInSourceTree(
dbg,
[
"index.html",
"named-eval.js",
"normal-script.js",
"slow-loading-script.js",
"same-url.js",
"iframe.html",
"same-url.js",
"new-function.js",
],
{
noExpand: true,
}
);
await selectSourceFromSourceTree(
dbg,
"same-url.js",
12,
"Select `same-url.js` in the iframe"
);
is(
getCM(dbg).getValue(),
`console.log("same url #3")`,
"We get the expected content for same-url.js in the iframe"
);
const iframeThread = dbg.selectors
.getAllThreads()
.find(thread => thread.name == `${BASE_URL}iframe.html`);
is(
sourceActors.filter(actor => actor.thread == iframeThread.actor).length,
1,
"same-url.js is loaded one time in the iframe thread"
);
} else {
// There is no iframe thread when fission is off
const mainThread = dbg.selectors
.getAllThreads()
.find(thread => thread.name == "Main Thread");
is(
sourceActors.filter(actor => actor.thread == mainThread.actor).length,
4,
"same-url.js is loaded 4 times in the main thread without fission"
);
}
info(`Close the same-url.js from the iframe`);
await closeTab(dbg, "same-url.js");
info("Click on the worker tree node to show sources in the worker");
await clickElement(dbg, "sourceDirectoryLabel", noFissionNoEFT ? 10 : 13);
const workerSources = [
"index.html",
"named-eval.js",
"normal-script.js",
"slow-loading-script.js",
"same-url.js",
"iframe.html",
"same-url.js",
"new-function.js",
];
if (!noFissionNoEFT) {
workerSources.push("same-url.js");
}
await waitForSourcesInSourceTree(dbg, workerSources, {
noExpand: true,
});
await selectSourceFromSourceTree(
dbg,
"same-url.js",
noFissionNoEFT ? 12 : 15,
"Select `same-url.js` in the worker"
);
is(
getCM(dbg).getValue(),
`console.log("same url #4")`,
"We get the expected content for same-url.js worker"
);
const workerThread = dbg.selectors
.getAllThreads()
.find(thread => thread.name == `${BASE_URL}same-url.js`);
is(
sourceActors.filter(actor => actor.thread == workerThread.actor).length,
1,
"same-url.js is loaded one time in the worker thread"
);
await selectSource(dbg, "iframe.html");
is(getCM(dbg).getValue(), IFRAME_CONTENT);
ok(
!sourceExists(dbg, "http-error-script.js"),
"scripts with HTTP error code do not appear in the source list"
);
info(
"Verify that breaking in a source without url displays the right content"
);
let onNewSource = waitForDispatch(dbg.store, "ADD_SOURCES");
invokeInTab("breakInNewFunction");
await waitForPaused(dbg);
let { sources } = await onNewSource;
is(sources.length, 1, "Got a unique source related to new Function source");
let newFunctionSource = sources[0];
// We acknowledge the function header as well as the new line in the first argument
assertPausedAtSourceAndLine(dbg, newFunctionSource.id, 4, 0);
is(getCM(dbg).getValue(), "function anonymous(a\n,b1\n) {\ndebugger;\n}");
await resume(dbg);
info(
"Break a second time in a source without url to verify we display the right content"
);
onNewSource = waitForDispatch(dbg.store, "ADD_SOURCES");
invokeInTab("breakInNewFunction");
await waitForPaused(dbg);
({ sources } = await onNewSource);
is(sources.length, 1, "Got a unique source related to new Function source");
newFunctionSource = sources[0];
// We acknowledge the function header as well as the new line in the first argument
assertPausedAtSourceAndLine(dbg, newFunctionSource.id, 4, 0);
is(getCM(dbg).getValue(), "function anonymous(a\n,b2\n) {\ndebugger;\n}");
await resume(dbg);
// As we are loading the page while the debugger is already opened,
// none of the resources are loaded twice.
is(loadCounts["/index.html"], 1, "We loaded index.html only once");
is(
loadCounts["/normal-script.js"],
1,
"We loaded normal-script.js only once"
);
is(
loadCounts["/slow-loading-script.js"],
1,
"We loaded slow-loading-script.js only once"
);
is(
loadCounts["/same-url.js"],
4,
"We loaded same-url.js in 4 distinct ways (the named eval doesn't count)"
);
// For some reason external to the debugger, we issue two requests to scripts having http error codes.
// These two requests are done before opening the debugger.
is(
loadCounts["/http-error-script.js"],
2,
"We loaded http-error-script.js twice, only before the debugger is opened"
);
});
/**
* In this test, we force a GC before loading DevTools.
* So that Spidermonkey will no longer have access to the sources
* and another request should be issues to load the source text content.
*/
const GARBAGED_PAGE_CONTENT = `
`;
httpServer.registerPathHandler(
"/garbaged-collected.html",
(request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(GARBAGED_PAGE_CONTENT);
}
);
httpServer.registerPathHandler("/garbaged-script.js", (request, response) => {
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
response.setHeader("Content-Type", "application/javascript");
response.write(`console.log("garbaged script ${loadCounts[request.path]}")`);
});
add_task(async function testGarbageCollectedSourceTextContent() {
const tab = await addTab(BASE_URL + "garbaged-collected.html");
is(
loadCounts["/garbaged-collected.html"],
1,
"The HTML page is loaded once before opening the DevTools"
);
is(
loadCounts["/garbaged-script.js"],
1,
"The script is loaded once before opening the DevTools"
);
// Force freeing both the HTML page and script in memory
// so that the debugger has to fetch source content from http cache.
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
Cu.forceGC();
});
const toolbox = await openToolboxForTab(tab, "jsdebugger");
const dbg = createDebuggerContext(toolbox);
await waitForSources(dbg, "garbaged-collected.html", "garbaged-script.js");
await selectSource(dbg, "garbaged-script.js");
// XXX Bug 1758454 - Source content of GC-ed script can be wrong!
// Even if we have to issue a new HTTP request for this source,
// we should be using HTTP cache and retrieve the first served version which
// is the one that actually runs in the page!
// We should be displaying `console.log("garbaged script 1")`,
// but instead, a new HTTP request is dispatched and we get a new content.
is(getCM(dbg).getValue(), `console.log("garbaged script 2")`);
await selectSource(dbg, "garbaged-collected.html");
is(getCM(dbg).getValue(), GARBAGED_PAGE_CONTENT);
is(
loadCounts["/garbaged-collected.html"],
2,
"We loaded the html page once as we haven't tried to display it in the debugger (2)"
);
is(
loadCounts["/garbaged-script.js"],
2,
"We loaded the garbaged script twice as we lost its content"
);
});
/**
* Test failures when trying to open the source text content.
*
* In this test we load an html page
* - with inline source (so that it shows up in the debugger)
* - it first loads fine so that it shows up
* - initDebuggerWithAbsoluteURL will first load the document before the debugger
* - so the debugger will have to fetch the html page content via a network request
* - the test page will return a connection reset error on the second load attempt
*/
let loadCount = 0;
httpServer.registerPathHandler(
"/200-then-connection-reset.html",
(request, response) => {
loadCount++;
if (loadCount > 1) {
response.seizePower();
response.bodyOutPutStream.close();
response.finish();
return;
}
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(``);
}
);
add_task(async function testFailingHtmlSource() {
info("Test failure in retrieving html page sources");
// initDebuggerWithAbsoluteURL will first load the document once before the debugger,
// then the debugger will have to fetch the html page content via a network request
// therefore the test page will encounter a connection reset error on the second load attempt
const dbg = await initDebuggerWithAbsoluteURL(
BASE_URL + "200-then-connection-reset.html",
"200-then-connection-reset.html"
);
// We can't select the HTML page as its source content isn't fetched
// (waitForSelectedSource doesn't resolve)
// Note that it is important to load the page *before* opening the page
// so that the thread actor has to request the page content and will fail
const source = findSource(dbg, "200-then-connection-reset.html");
await dbg.actions.selectLocation(createLocation({ source }), {
keepContext: false,
});
ok(
getCM(dbg).getValue().includes("Could not load the source"),
"Display failure error"
);
});
/**
* In this test we try to reproduce the "Loading..." message.
* This may happen when opening an HTML source that was loaded *before*
* opening DevTools. The thread actor will have to issue a new HTTP request
* to load the source content.
*/
let loadCount2 = 0;
let slowLoadingPageResolution = null;
httpServer.registerPathHandler(
"/slow-loading-page.html",
(request, response) => {
loadCount2++;
if (loadCount2 > 1) {
response.processAsync();
slowLoadingPageResolution = function () {
response.write(
``
);
response.finish();
};
return;
}
response.write(
``
);
}
);
add_task(async function testLoadingHtmlSource() {
info("Test loading progress of html page sources");
const dbg = await initDebuggerWithAbsoluteURL(
BASE_URL + "slow-loading-page.html",
"slow-loading-page.html"
);
const onSelected = selectSource(dbg, "slow-loading-page.html");
await waitFor(
() => getCM(dbg).getValue() == DEBUGGER_L10N.getStr("loadingText"),
"Wait for the source to be displayed as loading"
);
info("Wait for a second HTTP request to be made for the html page");
await waitFor(
() => slowLoadingPageResolution,
"Wait for the html page to be queried a second time"
);
is(
getCM(dbg).getValue(),
DEBUGGER_L10N.getStr("loadingText"),
"The source is still loading until we release the network request"
);
slowLoadingPageResolution();
info("Wait for the source to be fully selected and loaded");
await onSelected;
// Note that, even if the thread actor triggers a new HTTP request,
// it will use the HTTP cache and retrieve the first request content.
// This is actually relevant as that's the source that actually runs in the page!
//
// XXX Bug 1758458 - the source content is wrong.
// We should be seeing the whole HTML page content,
// whereas we only see the inline source text content.
is(getCM(dbg).getValue(), `console.log("slow-loading-page:first-load");`);
});