summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/xpcshell/test_promises_run_to_completion.js
blob: 4d1e8745fee00b951a500d407a96b7034797188c (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
// 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 = createTestGlobal(
    "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(
    "Test(test_promises_run_to_completion.js:force_microtask_checkpoint)",
    () => ran
  );
}

add_test(test_promises_run_to_completion);