/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the ResourceCommand API around THREAD_STATE
const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
const BREAKPOINT_TEST_URL = URL_ROOT_SSL + "breakpoint_document.html";
const REMOTE_IFRAME_URL =
"https://example.org/document-builder.sjs?html=" +
encodeURIComponent("");
add_task(async function () {
// Check hitting the "debugger;" statement before and after calling
// watchResource(THREAD_TYPES). Both should break. First will
// be a cached resource and second will be a live one.
await checkBreakpointBeforeWatchResources();
await checkBreakpointAfterWatchResources();
// Check setting a real breakpoint on a given line
await checkRealBreakpoint();
// Check the "pause on exception" setting
await checkPauseOnException();
// Check an edge case where spamming setBreakpoints calls causes issues
await checkSetBeforeWatch();
// Check debugger statement for (remote) iframes
await checkDebuggerStatementInIframes();
});
async function checkBreakpointBeforeWatchResources() {
info(
"Check whether ResourceCommand gets existing breakpoint, being hit before calling watchResources"
);
const tab = await addTab(BREAKPOINT_TEST_URL);
const { client, resourceCommand, targetCommand } = await initResourceCommand(
tab
);
// Ensure that the target front is initialized early from TargetCommand.onTargetAvailable
// By the time `initResourceCommand` resolves, it should already be initialized.
info(
"Verify that TargetFront's initialized is resolved after having calling attachAndInitThread"
);
await targetCommand.targetFront.initialized;
info("Run the 'debugger' statement");
// Note that we do not wait for the resolution of spawn as it will be paused
ContentTask.spawn(tab.linkedBrowser, null, () => {
content.window.wrappedJSObject.runDebuggerStatement();
});
info("Call watchResources");
const availableResources = [];
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: resources => availableResources.push(...resources),
});
is(
availableResources.length,
1,
"Got the THREAD_STATE's related to the debugger statement"
);
const threadState = availableResources.pop();
assertPausedResource(threadState, {
state: "paused",
why: {
type: "debuggerStatement",
},
frame: {
type: "call",
asyncCause: null,
state: "on-stack",
// this: object actor's form referring to `this` variable
displayName: "runDebuggerStatement",
// arguments: []
where: {
line: 17,
column: 6,
},
},
});
const { threadFront } = targetCommand.targetFront;
await threadFront.resume();
await waitFor(
() => availableResources.length == 1,
"Wait until we receive the resumed event"
);
const resumed = availableResources.pop();
assertResumedResource(resumed);
targetCommand.destroy();
await client.close();
}
async function checkBreakpointAfterWatchResources() {
info(
"Check whether ResourceCommand gets breakpoint hit after calling watchResources"
);
const tab = await addTab(BREAKPOINT_TEST_URL);
const { client, resourceCommand, targetCommand } = await initResourceCommand(
tab
);
info("Call watchResources");
const availableResources = [];
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: resources => availableResources.push(...resources),
});
is(
availableResources.length,
0,
"Got no THREAD_STATE when calling watchResources"
);
info("Run the 'debugger' statement");
// Note that we do not wait for the resolution of spawn as it will be paused
ContentTask.spawn(tab.linkedBrowser, null, () => {
content.window.wrappedJSObject.runDebuggerStatement();
});
await waitFor(
() => availableResources.length == 1,
"Got the THREAD_STATE related to the debugger statement"
);
const threadState = availableResources.pop();
assertPausedResource(threadState, {
state: "paused",
why: {
type: "debuggerStatement",
},
frame: {
type: "call",
asyncCause: null,
state: "on-stack",
// this: object actor's form referring to `this` variable
displayName: "runDebuggerStatement",
// arguments: []
where: {
line: 17,
column: 6,
},
},
});
// treadFront is created and attached while calling watchResources
const { threadFront } = targetCommand.targetFront;
await threadFront.resume();
await waitFor(
() => availableResources.length == 1,
"Wait until we receive the resumed event"
);
const resumed = availableResources.pop();
assertResumedResource(resumed);
targetCommand.destroy();
await client.close();
}
async function checkRealBreakpoint() {
info(
"Check whether ResourceCommand gets breakpoint set via the thread Front (instead of just debugger statements)"
);
const tab = await addTab(BREAKPOINT_TEST_URL);
const { client, resourceCommand, targetCommand } = await initResourceCommand(
tab
);
info("Call watchResources");
const availableResources = [];
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: resources => availableResources.push(...resources),
});
is(
availableResources.length,
0,
"Got no THREAD_STATE when calling watchResources"
);
// treadFront is created and attached while calling watchResources
const { threadFront } = targetCommand.targetFront;
// We have to call `sources` request, otherwise the Thread Actor
// doesn't start watching for sources, and ignore the setBreakpoint call
// as it doesn't have any source registered.
await threadFront.getSources();
await threadFront.setBreakpoint(
{ sourceUrl: BREAKPOINT_TEST_URL, line: 14 },
{}
);
info("Run the test function where we set a breakpoint");
// Note that we do not wait for the resolution of spawn as it will be paused
ContentTask.spawn(tab.linkedBrowser, null, () => {
content.window.wrappedJSObject.testFunction();
});
await waitFor(
() => availableResources.length == 1,
"Got the THREAD_STATE related to the debugger statement"
);
const threadState = availableResources.pop();
assertPausedResource(threadState, {
state: "paused",
why: {
type: "breakpoint",
},
frame: {
type: "call",
asyncCause: null,
state: "on-stack",
// this: object actor's form referring to `this` variable
displayName: "testFunction",
// arguments: []
where: {
line: 14,
column: 6,
},
},
});
await threadFront.resume();
await waitFor(
() => availableResources.length == 1,
"Wait until we receive the resumed event"
);
const resumed = availableResources.pop();
assertResumedResource(resumed);
targetCommand.destroy();
await client.close();
}
async function checkPauseOnException() {
info(
"Check whether ResourceCommand gets breakpoint for exception (when explicitly requested)"
);
const tab = await addTab(
"data:text/html,"
);
const { commands, resourceCommand, targetCommand } =
await initResourceCommand(tab);
info("Call watchResources");
const availableResources = [];
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: resources => availableResources.push(...resources),
});
is(
availableResources.length,
0,
"Got no THREAD_STATE when calling watchResources"
);
await commands.threadConfigurationCommand.updateConfiguration({
pauseOnExceptions: true,
});
info("Reload the page, in order to trigger exception on load");
const reloaded = reloadBrowser();
await waitFor(
() => availableResources.length == 1,
"Got the THREAD_STATE related to the debugger statement"
);
const threadState = availableResources.pop();
assertPausedResource(threadState, {
state: "paused",
why: {
type: "exception",
},
frame: {
type: "global",
asyncCause: null,
state: "on-stack",
// this: object actor's form referring to `this` variable
displayName: "(global)",
// arguments: []
where: {
line: 1,
column: 27,
},
},
});
const { threadFront } = targetCommand.targetFront;
await threadFront.resume();
info("Wait for page to finish reloading after resume");
await reloaded;
await waitFor(
() => availableResources.length == 1,
"Wait until we receive the resumed event"
);
const resumed = availableResources.pop();
assertResumedResource(resumed);
targetCommand.destroy();
await commands.destroy();
}
async function checkSetBeforeWatch() {
info(
"Verify bug 1683139 - D103068, where setting a breakpoint before watching for thread state, avoid receiving the paused state"
);
const tab = await addTab(BREAKPOINT_TEST_URL);
const { client, resourceCommand, targetCommand } = await initResourceCommand(
tab
);
// Instantiate the thread front in order to be able to set a breakpoint before watching for thread state
info("Attach the top level thread actor");
await targetCommand.targetFront.attachAndInitThread(targetCommand);
const { threadFront } = targetCommand.targetFront;
// We have to call `sources` request, otherwise the Thread Actor
// doesn't start watching for sources, and ignore the setBreakpoint call
// as it doesn't have any source registered.
await threadFront.getSources();
// Set the breakpoint before trying to hit it
await threadFront.setBreakpoint(
{ sourceUrl: BREAKPOINT_TEST_URL, line: 14, column: 6 },
{}
);
info("Run the test function where we set a breakpoint");
// Note that we do not wait for the resolution of spawn as it will be paused
ContentTask.spawn(tab.linkedBrowser, null, () => {
content.window.wrappedJSObject.testFunction();
});
// bug 1683139 - D103068. Re-setting the breakpoint just before watching for thread state
// prevented to receive the paused state change.
await threadFront.setBreakpoint(
{ sourceUrl: BREAKPOINT_TEST_URL, line: 14, column: 6 },
{}
);
info("Call watchResources");
const availableResources = [];
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: resources => availableResources.push(...resources),
});
await waitFor(
() => availableResources.length == 1,
"Got the THREAD_STATE related to the debugger statement"
);
const threadState = availableResources.pop();
assertPausedResource(threadState, {
state: "paused",
why: {
type: "breakpoint",
},
frame: {
type: "call",
asyncCause: null,
state: "on-stack",
// this: object actor's form referring to `this` variable
displayName: "testFunction",
// arguments: []
where: {
line: 14,
column: 6,
},
},
});
await threadFront.resume();
await waitFor(
() => availableResources.length == 1,
"Wait until we receive the resumed event"
);
const resumed = availableResources.pop();
assertResumedResource(resumed);
targetCommand.destroy();
await client.close();
}
async function checkDebuggerStatementInIframes() {
info("Check whether ResourceCommand gets breakpoint for (remote) iframes");
const tab = await addTab(BREAKPOINT_TEST_URL);
const { client, resourceCommand, targetCommand } = await initResourceCommand(
tab
);
info("Call watchResources");
const availableResources = [];
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: resources => availableResources.push(...resources),
});
is(
availableResources.length,
0,
"Got no THREAD_STATE when calling watchResources"
);
info("Inject the iframe with an inline 'debugger' statement");
// Note that we do not wait for the resolution of spawn as it will be paused
SpecialPowers.spawn(
gBrowser.selectedBrowser,
[REMOTE_IFRAME_URL],
async function (url) {
const iframe = content.document.createElement("iframe");
iframe.src = url;
content.document.body.appendChild(iframe);
}
);
await waitFor(
() => availableResources.length == 1,
"Got the THREAD_STATE related to the iframe's debugger statement"
);
const threadState = availableResources.pop();
assertPausedResource(threadState, {
state: "paused",
why: {
type: "debuggerStatement",
},
frame: {
type: "global",
asyncCause: null,
state: "on-stack",
// this: object actor's form referring to `this` variable
displayName: "(global)",
// arguments: []
where: {
line: 1,
column: 8,
},
},
});
const iframeTarget = threadState.targetFront;
if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
is(
iframeTarget.url,
REMOTE_IFRAME_URL,
"With fission/EFT, the pause is from the iframe's target"
);
} else {
is(
iframeTarget,
targetCommand.targetFront,
"Without fission/EFT, the pause is from the top level target"
);
}
const { threadFront } = iframeTarget;
await threadFront.resume();
await waitFor(
() => availableResources.length == 1,
"Wait until we receive the resumed event"
);
const resumed = availableResources.pop();
assertResumedResource(resumed);
targetCommand.destroy();
await client.close();
}
async function assertPausedResource(resource, expected) {
is(
resource.resourceType,
ResourceCommand.TYPES.THREAD_STATE,
"Resource type is correct"
);
is(resource.state, "paused", "state attribute is correct");
is(resource.why.type, expected.why.type, "why.type attribute is correct");
is(
resource.frame.type,
expected.frame.type,
"frame.type attribute is correct"
);
is(
resource.frame.asyncCause,
expected.frame.asyncCause,
"frame.asyncCause attribute is correct"
);
is(
resource.frame.state,
expected.frame.state,
"frame.state attribute is correct"
);
is(
resource.frame.displayName,
expected.frame.displayName,
"frame.displayName attribute is correct"
);
is(
resource.frame.where.line,
expected.frame.where.line,
"frame.where.line attribute is correct"
);
is(
resource.frame.where.column,
expected.frame.where.column,
"frame.where.column attribute is correct"
);
}
async function assertResumedResource(resource) {
is(
resource.resourceType,
ResourceCommand.TYPES.THREAD_STATE,
"Resource type is correct"
);
is(resource.state, "resumed", "state attribute is correct");
}