mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-26 01:35:08 +02:00
When an async function is resumed from a microtask and hits another await with a non-thenable value (primitive or already-settled native promise), and the microtask queue is empty, we can resolve the await synchronously without suspending. No other microtask can observe the difference in execution order, making this optimization safe. This avoids the overhead of creating a GC::Function for the microtask job, enqueuing/dequeuing from the microtask queue, and the execution context push/pop that comes with it. A new VM host hook, host_promise_job_queue_is_empty, is added so both the standalone js binary and LibWeb can provide the appropriate check for their respective job queue implementations.
760 lines
22 KiB
JavaScript
760 lines
22 KiB
JavaScript
describe("parsing freestanding async functions", () => {
|
|
test("simple", () => {
|
|
expect(`async function foo() {}`).toEval();
|
|
// Although it does not create an async function it is valid.
|
|
expect(`async
|
|
function foo() {}`).toEval();
|
|
|
|
expect(`async function await() {}`).toEval();
|
|
expect(`async function yield() {}`).toEval();
|
|
});
|
|
test("await expression", () => {
|
|
expect(`async function foo() { await bar(); }`).toEval();
|
|
expect(`async function foo() { await; }`).not.toEval();
|
|
expect(`function foo() { await bar(); }`).not.toEval();
|
|
expect(`function foo() { await; }`).toEval();
|
|
|
|
expect(`\\u0061sync function foo() { await bar(); }`).not.toEval();
|
|
expect(`\\u0061sync function foo() { \\u0061wait bar(); }`).not.toEval();
|
|
expect(`async function foo() { \\u0061wait bar(); }`).not.toEval();
|
|
});
|
|
});
|
|
|
|
describe("parsing object literal async functions", () => {
|
|
test("simple", () => {
|
|
expect(`x = { async foo() { } }`).toEval();
|
|
expect(`x = { async
|
|
foo() { } }`).not.toEval();
|
|
});
|
|
|
|
test("property on object called async", () => {
|
|
expect(`x = { async() { } }`).toEval();
|
|
expect(`x = { async() { await 4; } }`).not.toEval();
|
|
expect(`x = { async: 3 }`).toEval();
|
|
expect(`x = { async: await 3, }`).not.toEval();
|
|
});
|
|
|
|
test("await expression", () => {
|
|
expect(`x = { foo() { await bar(); } }`).not.toEval();
|
|
expect(`x = { foo() { await; } }`).toEval();
|
|
expect(`x = { async foo() { await bar(); } }`).toEval();
|
|
expect(`x = { async foo() { await; } }`).not.toEval();
|
|
});
|
|
});
|
|
|
|
describe("parsing classes with async methods", () => {
|
|
test("simple", () => {
|
|
expect(`class Foo { async foo() {} }`).toEval();
|
|
expect(`class Foo { static async foo() {} }`).toEval();
|
|
expect(`class Foo { async foo() { await bar(); } }`).toEval();
|
|
expect(`class Foo { async foo() { await; } }`).not.toEval();
|
|
expect(`class Foo { async constructor() {} }`).not.toEval();
|
|
});
|
|
});
|
|
|
|
test("function expression names equal to 'await'", () => {
|
|
expect(`async function foo() { (function await() {}); }`).toEval();
|
|
expect(`async function foo() { function await() {} }`).not.toEval();
|
|
});
|
|
|
|
test("async function cannot use await in default parameters", () => {
|
|
expect("async function foo(x = await 3) {}").not.toEval();
|
|
expect("async function foo(x = await 3) {}").not.toEval();
|
|
|
|
// Even as a reference to some variable it is not allowed
|
|
expect(`
|
|
var await = 4;
|
|
async function foo(x = await) {}
|
|
`).not.toEval();
|
|
});
|
|
|
|
describe("async arrow functions", () => {
|
|
test("basic syntax", () => {
|
|
expect("async () => await 3;").toEval();
|
|
expect("async param => await param();").toEval();
|
|
expect("async (param) => await param();").toEval();
|
|
expect("async (a, b) => await a();").toEval();
|
|
|
|
expect("async () => { await 3; }").toEval();
|
|
expect("async param => { await param(); }").toEval();
|
|
expect("async (param) => { await param(); }").toEval();
|
|
expect("async (a, b) => { await a(); }").toEval();
|
|
|
|
expect(`async
|
|
() => await 3;`).not.toEval();
|
|
|
|
expect("async async => await async()").toEval();
|
|
expect("async => async").toEval();
|
|
expect("async => await async()").not.toEval();
|
|
|
|
expect("async (b = await) => await b;").not.toEval();
|
|
expect("async (b = await 3) => await b;").not.toEval();
|
|
|
|
// Cannot escape the async keyword and get an async arrow function.
|
|
expect("\\u0061sync () => await 3").not.toEval();
|
|
|
|
expect("for (async of => {};false;) {}").toEval();
|
|
expect("for (async of []) {}").not.toEval();
|
|
|
|
expect("for (\\u0061sync of []) {}").toEval();
|
|
expect("for (\\u0061sync of => {};false;) {}").not.toEval();
|
|
expect("for (\\u0061sync => {};false;) {}").toEval();
|
|
});
|
|
|
|
test("async within a for-loop", () => {
|
|
let called = false;
|
|
// Unfortunately we cannot really test the more horrible case above.
|
|
for (
|
|
const f = async of => {
|
|
return of;
|
|
};
|
|
;
|
|
) {
|
|
expect(f(43)).toBeInstanceOf(Promise);
|
|
|
|
called = true;
|
|
break;
|
|
}
|
|
expect(called).toBeTrue();
|
|
});
|
|
});
|
|
|
|
test("basic functionality", () => {
|
|
test("simple", () => {
|
|
let executionValue = null;
|
|
let resultValue = null;
|
|
async function foo() {
|
|
executionValue = "someValue";
|
|
return "otherValue";
|
|
}
|
|
const returnValue = foo();
|
|
expect(returnValue).toBeInstanceOf(Promise);
|
|
returnValue.then(result => {
|
|
resultValue = result;
|
|
});
|
|
runQueuedPromiseJobs();
|
|
expect(executionValue).toBe("someValue");
|
|
expect(resultValue).toBe("otherValue");
|
|
});
|
|
|
|
test("await", () => {
|
|
let resultValue = null;
|
|
async function foo() {
|
|
return "someValue";
|
|
}
|
|
async function bar() {
|
|
resultValue = await foo();
|
|
}
|
|
bar();
|
|
runQueuedPromiseJobs();
|
|
expect(resultValue).toBe("someValue");
|
|
});
|
|
});
|
|
|
|
describe("non async function declaration usage of async still works", () => {
|
|
test("async as a function", () => {
|
|
function async(value = 4) {
|
|
return value;
|
|
}
|
|
|
|
expect(async(0)).toBe(0);
|
|
|
|
// We use eval here since it otherwise cannot find the async function.
|
|
const evalResult = eval("async(1)");
|
|
expect(evalResult).toBe(1);
|
|
});
|
|
|
|
test("async as a variable", () => {
|
|
let async = 3;
|
|
|
|
const evalResult = eval("async >= 2");
|
|
expect(evalResult).toBeTrue();
|
|
});
|
|
|
|
test("async with line ending does not create a function", () => {
|
|
expect(() => {
|
|
// The ignore is needed otherwise prettier puts a ';' after async.
|
|
// prettier-ignore
|
|
async
|
|
function f() {}
|
|
}).toThrowWithMessage(ReferenceError, "'async' is not defined");
|
|
|
|
expect(`async
|
|
function f() { await 3; }`).not.toEval();
|
|
});
|
|
});
|
|
|
|
describe("await cannot be used in class static init blocks", () => {
|
|
test("directly", () => {
|
|
expect("class A{ static { await; } }").not.toEval();
|
|
expect("class A{ static { let await = 3; } }").not.toEval();
|
|
expect("class A{ static { call(await); } }").not.toEval();
|
|
expect("class A{ static { for(const await = 1; false ;) {} } }").not.toEval();
|
|
});
|
|
|
|
test("via declaration", () => {
|
|
expect("class A{ static { class await {} } }").not.toEval();
|
|
expect("class A{ static { function await() {} } }").not.toEval();
|
|
expect("class A{ static { function* await() {} } }").not.toEval();
|
|
expect("class A{ static { async function* await() {} } }").not.toEval();
|
|
});
|
|
});
|
|
|
|
describe("await thenables", () => {
|
|
test("async returning a thanable variable without fulfilling", () => {
|
|
let isCalled = false;
|
|
const obj = {
|
|
then() {
|
|
isCalled = true;
|
|
},
|
|
};
|
|
|
|
const f = async () => await obj;
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(isCalled).toBe(true);
|
|
});
|
|
|
|
test("async returning a thanable variable that fulfills", () => {
|
|
let isCalled = false;
|
|
const obj = {
|
|
then(fulfill) {
|
|
isCalled = true;
|
|
fulfill(isCalled);
|
|
},
|
|
};
|
|
|
|
const f = async () => await obj;
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(isCalled).toBe(true);
|
|
});
|
|
|
|
test("async returning a thenable directly without fulfilling", () => {
|
|
let isCalled = false;
|
|
const f = async () => ({
|
|
then() {
|
|
isCalled = true;
|
|
},
|
|
});
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(isCalled).toBe(true);
|
|
});
|
|
|
|
test("async returning a thenable directly that fulfills", () => {
|
|
let isCalled = false;
|
|
const f = async () => ({
|
|
then(fulfill) {
|
|
isCalled = true;
|
|
fulfill(isCalled);
|
|
},
|
|
});
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(isCalled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("await primitive values", () => {
|
|
test("await number", () => {
|
|
let result;
|
|
async function f() {
|
|
result = await 42;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(42);
|
|
});
|
|
|
|
test("await string", () => {
|
|
let result;
|
|
async function f() {
|
|
result = await "hello";
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe("hello");
|
|
});
|
|
|
|
test("await boolean", () => {
|
|
let result;
|
|
async function f() {
|
|
result = await true;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBeTrue();
|
|
});
|
|
|
|
test("await null", () => {
|
|
let result = "not null";
|
|
async function f() {
|
|
result = await null;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("await undefined", () => {
|
|
let result = "not undefined";
|
|
async function f() {
|
|
result = await undefined;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
test("await symbol", () => {
|
|
const sym = Symbol("test");
|
|
let result;
|
|
async function f() {
|
|
result = await sym;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(sym);
|
|
});
|
|
|
|
test("await bigint", () => {
|
|
let result;
|
|
async function f() {
|
|
result = await 123n;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(123n);
|
|
});
|
|
|
|
test("multiple primitive awaits in sequence", () => {
|
|
const results = [];
|
|
async function f() {
|
|
results.push(await 1);
|
|
results.push(await "two");
|
|
results.push(await true);
|
|
results.push(await null);
|
|
results.push(await undefined);
|
|
results.push(await 42n);
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(results).toEqual([1, "two", true, null, undefined, 42n]);
|
|
});
|
|
});
|
|
|
|
describe("await already-settled native promises", () => {
|
|
test("await resolved promise", () => {
|
|
let result;
|
|
async function f() {
|
|
result = await Promise.resolve(99);
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(99);
|
|
});
|
|
|
|
test("await rejected promise in try-catch", () => {
|
|
let caught;
|
|
async function f() {
|
|
try {
|
|
await Promise.reject("oops");
|
|
} catch (e) {
|
|
caught = e;
|
|
}
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(caught).toBe("oops");
|
|
});
|
|
|
|
test("await rejected promise propagates to caller", () => {
|
|
let caught;
|
|
async function f() {
|
|
await Promise.reject(new Error("fail"));
|
|
}
|
|
f().catch(e => {
|
|
caught = e.message;
|
|
});
|
|
runQueuedPromiseJobs();
|
|
expect(caught).toBe("fail");
|
|
});
|
|
|
|
test("multiple resolved promises in sequence", () => {
|
|
const results = [];
|
|
async function f() {
|
|
results.push(await Promise.resolve("a"));
|
|
results.push(await Promise.resolve("b"));
|
|
results.push(await Promise.resolve("c"));
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(results).toEqual(["a", "b", "c"]);
|
|
});
|
|
|
|
test("await resolved promise with undefined value", () => {
|
|
let result = "not undefined";
|
|
async function f() {
|
|
result = await Promise.resolve(undefined);
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
test("await resolved promise with object value", () => {
|
|
const obj = { key: "value" };
|
|
let result;
|
|
async function f() {
|
|
result = await Promise.resolve(obj);
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(obj);
|
|
});
|
|
});
|
|
|
|
describe("await pending promises", () => {
|
|
test("await promise that resolves later", () => {
|
|
let result;
|
|
let resolve;
|
|
const p = new Promise(r => {
|
|
resolve = r;
|
|
});
|
|
async function f() {
|
|
result = await p;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBeUndefined();
|
|
resolve(42);
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(42);
|
|
});
|
|
|
|
test("await promise that rejects later", () => {
|
|
let caught;
|
|
let reject;
|
|
const p = new Promise((_, r) => {
|
|
reject = r;
|
|
});
|
|
async function f() {
|
|
try {
|
|
await p;
|
|
} catch (e) {
|
|
caught = e;
|
|
}
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(caught).toBeUndefined();
|
|
reject("error");
|
|
runQueuedPromiseJobs();
|
|
expect(caught).toBe("error");
|
|
});
|
|
});
|
|
|
|
describe("await promises with own properties or non-standard prototype", () => {
|
|
test("await resolved promise with own property takes slow path", () => {
|
|
let result;
|
|
async function f() {
|
|
const p = Promise.resolve(77);
|
|
p.extraProp = true;
|
|
result = await p;
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(77);
|
|
});
|
|
|
|
test("await rejected promise with own property takes slow path", () => {
|
|
let caught;
|
|
async function f() {
|
|
const p = Promise.reject("err");
|
|
p.extraProp = true;
|
|
try {
|
|
await p;
|
|
} catch (e) {
|
|
caught = e;
|
|
}
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(caught).toBe("err");
|
|
});
|
|
|
|
test("await promise subclass", () => {
|
|
class MyPromise extends Promise {}
|
|
let result;
|
|
async function f() {
|
|
result = await MyPromise.resolve(55);
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(result).toBe(55);
|
|
});
|
|
});
|
|
|
|
describe("microtask ordering with await", () => {
|
|
test("await primitive vs Promise.resolve().then()", () => {
|
|
const order = [];
|
|
async function f() {
|
|
await 1;
|
|
order.push("async");
|
|
}
|
|
f();
|
|
Promise.resolve().then(() => order.push("then"));
|
|
runQueuedPromiseJobs();
|
|
expect(order).toEqual(["async", "then"]);
|
|
});
|
|
|
|
test("await resolved promise vs Promise.resolve().then()", () => {
|
|
const order = [];
|
|
async function f() {
|
|
await Promise.resolve(1);
|
|
order.push("async");
|
|
}
|
|
f();
|
|
Promise.resolve().then(() => order.push("then"));
|
|
runQueuedPromiseJobs();
|
|
expect(order).toEqual(["async", "then"]);
|
|
});
|
|
|
|
test("fast path and slow path produce same interleaving order", () => {
|
|
// Test that the fast path (primitives) and slow path (objects)
|
|
// produce the same microtask interleaving behavior.
|
|
const fastOrder = [];
|
|
async function fastF1() {
|
|
await 1;
|
|
fastOrder.push("f1-a");
|
|
await 2;
|
|
fastOrder.push("f1-b");
|
|
}
|
|
async function fastF2() {
|
|
await 3;
|
|
fastOrder.push("f2-a");
|
|
await 4;
|
|
fastOrder.push("f2-b");
|
|
}
|
|
fastF1();
|
|
fastF2();
|
|
runQueuedPromiseJobs();
|
|
|
|
const slowOrder = [];
|
|
async function slowF1() {
|
|
await {};
|
|
slowOrder.push("f1-a");
|
|
await {};
|
|
slowOrder.push("f1-b");
|
|
}
|
|
async function slowF2() {
|
|
await {};
|
|
slowOrder.push("f2-a");
|
|
await {};
|
|
slowOrder.push("f2-b");
|
|
}
|
|
slowF1();
|
|
slowF2();
|
|
runQueuedPromiseJobs();
|
|
|
|
expect(fastOrder).toEqual(slowOrder);
|
|
});
|
|
|
|
test("fast path and slow path produce same mixed interleaving", () => {
|
|
const fastOrder = [];
|
|
async function fastF1() {
|
|
await 42;
|
|
fastOrder.push("f1-a");
|
|
await Promise.resolve(1);
|
|
fastOrder.push("f1-b");
|
|
}
|
|
async function fastF2() {
|
|
await Promise.resolve(2);
|
|
fastOrder.push("f2-a");
|
|
await 99;
|
|
fastOrder.push("f2-b");
|
|
}
|
|
fastF1();
|
|
fastF2();
|
|
runQueuedPromiseJobs();
|
|
|
|
const slowOrder = [];
|
|
async function slowF1() {
|
|
await {};
|
|
slowOrder.push("f1-a");
|
|
await {};
|
|
slowOrder.push("f1-b");
|
|
}
|
|
async function slowF2() {
|
|
await {};
|
|
slowOrder.push("f2-a");
|
|
await {};
|
|
slowOrder.push("f2-b");
|
|
}
|
|
slowF1();
|
|
slowF2();
|
|
runQueuedPromiseJobs();
|
|
|
|
expect(fastOrder).toEqual(slowOrder);
|
|
});
|
|
});
|
|
|
|
// The synchronous await fast path fires when:
|
|
// 1. m_is_initial_execution is false (i.e. we've already suspended once), AND
|
|
// 2. the microtask queue is empty (no other async work in flight)
|
|
// This means it activates on the 2nd+ await in a single async function
|
|
// when no other async functions are running concurrently.
|
|
describe("synchronous await fast path", () => {
|
|
test("multiple rejected promises in sequence", () => {
|
|
const caught = [];
|
|
async function f() {
|
|
for (const v of [1, 2, 3]) {
|
|
try {
|
|
await Promise.reject(v);
|
|
} catch (e) {
|
|
caught.push(e);
|
|
}
|
|
}
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(caught).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test("mixed resolved and rejected promises", () => {
|
|
const results = [];
|
|
async function f() {
|
|
results.push(await Promise.resolve("a"));
|
|
try {
|
|
await Promise.reject("b");
|
|
} catch (e) {
|
|
results.push("caught:" + e);
|
|
}
|
|
results.push(await 42);
|
|
results.push(await Promise.resolve("d"));
|
|
try {
|
|
await Promise.reject("e");
|
|
} catch (e) {
|
|
results.push("caught:" + e);
|
|
}
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(results).toEqual(["a", "caught:b", 42, "d", "caught:e"]);
|
|
});
|
|
|
|
test("promise with own property falls back to slow path", () => {
|
|
const results = [];
|
|
async function f() {
|
|
results.push(await Promise.resolve(1));
|
|
const p = Promise.resolve(2);
|
|
p.extraProp = true;
|
|
results.push(await p);
|
|
results.push(await Promise.resolve(3));
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(results).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test("promise subclass falls back to slow path", () => {
|
|
class MyPromise extends Promise {}
|
|
const results = [];
|
|
async function f() {
|
|
results.push(await Promise.resolve(1));
|
|
results.push(await MyPromise.resolve(2));
|
|
results.push(await Promise.resolve(3));
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(results).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
test("pending promise falls back to slow path", () => {
|
|
let resolve;
|
|
const results = [];
|
|
async function f() {
|
|
results.push(await Promise.resolve(1));
|
|
results.push(
|
|
await new Promise(r => {
|
|
resolve = r;
|
|
})
|
|
);
|
|
results.push(await Promise.resolve(3));
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
expect(results).toEqual([1]);
|
|
resolve(2);
|
|
runQueuedPromiseJobs();
|
|
expect(results).toEqual([1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
describe("await fast path does not invoke getters on Promise.prototype.constructor", () => {
|
|
test("getter on Promise.prototype.constructor causes fallback to slow path", () => {
|
|
let calls = 0;
|
|
const original = Object.getOwnPropertyDescriptor(Promise.prototype, "constructor");
|
|
Object.defineProperty(Promise.prototype, "constructor", {
|
|
get() {
|
|
calls++;
|
|
return Promise;
|
|
},
|
|
configurable: true,
|
|
});
|
|
try {
|
|
async function f() {
|
|
// These settled native promises have no own properties, but the
|
|
// fast path must detect the getter on Promise.prototype.constructor
|
|
// and fall back to the slow path (which invokes it per spec).
|
|
await Promise.resolve(1);
|
|
await Promise.resolve(2);
|
|
}
|
|
f();
|
|
runQueuedPromiseJobs();
|
|
// Exactly 2 calls: one per await, from the slow path's Get(x, "constructor").
|
|
// The fast path itself must not contribute any extra calls.
|
|
expect(calls).toBe(2);
|
|
} finally {
|
|
Object.defineProperty(Promise.prototype, "constructor", original);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("await observably looks up constructor of Promise objects", () => {
|
|
let calls = 0;
|
|
function makeConstructorObservable(promise) {
|
|
Object.defineProperty(promise, "constructor", {
|
|
get() {
|
|
calls++;
|
|
return Promise;
|
|
},
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
async function test() {
|
|
await makeConstructorObservable(Promise.resolve(1));
|
|
await makeConstructorObservable(
|
|
new Promise(resolve => {
|
|
resolve();
|
|
})
|
|
);
|
|
await makeConstructorObservable(new Boolean(true));
|
|
await makeConstructorObservable({});
|
|
await makeConstructorObservable(new Number(2));
|
|
try {
|
|
await makeConstructorObservable(Promise.reject(3));
|
|
} catch {}
|
|
try {
|
|
return makeConstructorObservable(Promise.reject(1));
|
|
} catch {
|
|
return 2;
|
|
}
|
|
}
|
|
test();
|
|
runQueuedPromiseJobs();
|
|
expect(calls).toBe(4);
|
|
});
|