gecko-dev/devtools/server/tests/unit/test_promises_run_to_comple...

128 строки
4.2 KiB
JavaScript

// Bug 1145201: Promise then-handlers can still be executed while the debugger is paused.
//
// When a promise is resolved, for each of its callbacks, a microtask is queued
// to run the callback. At various points, the HTML spec says the browser must
// "perform a microtask checkpoint", which means to draw microtasks from the
// queue and run them, until the queue is empty.
//
// The HTML spec is careful to perform a microtask checkpoint directly after
// each invocation of an event handler or DOM callback, so that code using
// promises can trust that its promise callbacks run promptly, in a
// deterministic order, without DOM events or other outside influences
// intervening.
//
// When the JavaScript debugger interrupts the execution of debuggee content
// code, it naturally must process events for its own user interface and promise
// callbacks. However, it must not run any debuggee microtasks. The debuggee has
// been interrupted in the midst of executing some other code, and the
// JavaScript spec promises developers: "Once execution of a Job is initiated,
// the Job always executes to completion. No other Job may be initiated until
// the currently running Job completes." [1] This promise would be broken if the
// debugger's own event processing ran debuggee microtasks during the
// interruption.
//
// Looking at things from the other side, a microtask checkpoint must be
// performed before returning from a debugger callback, rather than being put
// off until the debuggee performs its next microtask checkpoint, so that
// debugger microtasks are not interleaved with debuggee microtasks. A debuggee
// microtask could hit a breakpoint or otherwise re-enter the debugger, which
// might be quite surprised to see a new debugger callback begin before its
// previous promise callbacks could finish.
//
// [1]: https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues
"use strict";
const Debugger = require("Debugger");
function test_promises_run_to_completion() {
const g = testGlobal("test global for test_promises_run_to_completion.js");
const dbg = new Debugger(g);
g.Assert = Assert;
const log = [""];
g.log = log;
dbg.onDebuggerStatement = function handleDebuggerStatement(frame) {
dbg.onDebuggerStatement = undefined;
// Exercise the promise machinery: resolve a promise and perform a microtask
// queue. When called from a debugger hook, the debuggee's microtasks should not
// run.
log[0] += "debug-handler(";
Promise.resolve(42).then(v => {
Assert.equal(
v,
42,
"debugger callback promise handler got the right value"
);
log[0] += "debug-react";
});
log[0] += "(";
force_microtask_checkpoint();
log[0] += ")";
Promise.resolve(42).then(v => {
// The microtask running this callback should be handled as we leave the
// onDebuggerStatement Debugger callback, and should not be interleaved
// with debuggee microtasks.
log[0] += "(trailing)";
});
log[0] += ")";
};
// Evaluate some debuggee code that resolves a promise, and then enters the debugger.
Cu.evalInSandbox(
`
log[0] += "eval(";
Promise.resolve(42).then(function debuggeePromiseCallback(v) {
Assert.equal(v, 42, "debuggee promise handler got the right value");
// Debugger microtask checkpoints must not run debuggee microtasks, so
// this callback should run at the next microtask checkpoint *not*
// performed by the debugger.
log[0] += "eval-react";
});
log[0] += "debugger(";
debugger;
log[0] += "))";
`,
g
);
// Let other microtasks run. This should run the debuggee's promise callback.
log[0] += "final(";
force_microtask_checkpoint();
log[0] += ")";
Assert.equal(
log[0],
`\
eval(\
debugger(\
debug-handler(\
(debug-react)\
)\
(trailing)\
))\
final(\
eval-react\
)`,
"microtasks ran as expected"
);
run_next_test();
}
function force_microtask_checkpoint() {
// Services.tm.spinEventLoopUntilEmpty only performs a microtask checkpoint if
// there is actually an event to run. So make one up.
let ran = false;
Services.tm.dispatchToMainThread(() => {
ran = true;
});
Services.tm.spinEventLoopUntil(() => ran);
}
add_test(test_promises_run_to_completion);