packages/web-components/fast-element/src/observation/update-queue.spec.ts
import { expect } from "chai";
import { Updates } from "./update-queue.js";
const waitMilliseconds = 100;
const maxRecursion = 10;
function watchSetTimeoutForErrors<TError = any>() {
const errors: TError[] = [];
const originalSetTimeout = globalThis.setTimeout;
globalThis.setTimeout = (callback: Function, timeout: number) => {
return originalSetTimeout(() => {
try {
callback();
} catch(error) {
errors.push(error);
}
}, timeout)
}
return () => {
globalThis.setTimeout = originalSetTimeout;
return errors;
};
}
describe("The UpdateQueue", () => {
context("when updating DOM asynchronously", () => {
it("calls task in a future turn", done => {
let called = false;
Updates.enqueue(() => {
called = true;
done();
});
expect(called).to.equal(false);
});
it("calls task.call method in a future turn", done => {
let called = false;
Updates.enqueue({
call: () => {
called = true;
done();
}
});
expect(called).to.equal(false);
});
it("calls multiple tasks in order", done => {
const calls:number[] = [];
Updates.enqueue(() => {
calls.push(0);
});
Updates.enqueue(() => {
calls.push(1);
});
Updates.enqueue(() => {
calls.push(2);
});
expect(calls).to.eql([]);
setTimeout(() => {
expect(calls).to.eql([0, 1, 2]);
done();
}, waitMilliseconds);
});
it("calls tasks in breadth-first order", done => {
let calls: number[] = [];
Updates.enqueue(() => {
calls.push(0);
Updates.enqueue(() => {
calls.push(2);
Updates.enqueue(() => {
calls.push(5);
});
Updates.enqueue(() => {
calls.push(6);
});
});
Updates.enqueue(() => {
calls.push(3);
});
});
Updates.enqueue(() => {
calls.push(1);
Updates.enqueue(() => {
calls.push(4);
});
});
expect(calls).to.eql([]);
setTimeout(() => {
expect(calls).to.eql([0, 1, 2, 3, 4, 5, 6]);
done();
}, waitMilliseconds);
});
it("can schedule more than capacity tasks", done => {
const target = 1060;
const targetList: number[] = [];
for (var i=0; i<target; i++) {
targetList.push(i);
}
const newList: number[] = [];
for (var i=0; i < target; i++) {
(function(i) {
Updates.enqueue(() => {
newList.push(i);
});
})(i);
}
setTimeout(() => {
expect(newList).to.eql(targetList);
done();
}, waitMilliseconds);
});
it("can schedule more than capacity*2 tasks", done => {
const target = 2060;
const targetList: number[] = [];
for (var i=0; i<target; i++) {
targetList.push(i);
}
const newList: number[] = [];
for (var i=0; i<target; i++) {
(function(i) {
Updates.enqueue(() => {
newList.push(i);
});
})(i);
}
setTimeout(() => {
expect(newList).to.eql(targetList);
done();
}, waitMilliseconds);
});
it("can schedule tasks recursively", done => {
const steps: number[] = [];
Updates.enqueue(() => {
steps.push(0);
Updates.enqueue(() => {
steps.push(2);
Updates.enqueue(() => {
steps.push(4);
});
steps.push(3);
});
steps.push(1);
});
setTimeout(() => {
expect(steps).to.eql([0, 1, 2, 3, 4]);
done();
}, waitMilliseconds);
});
it(`can recurse ${maxRecursion} tasks deep`, done => {
let recurseCount = 0;
function go() {
if (++recurseCount < maxRecursion) {
Updates.enqueue(go);
}
}
Updates.enqueue(go);
setTimeout(() => {
expect(recurseCount).to.equal(maxRecursion);
done();
}, waitMilliseconds);
});
it("can execute two branches of recursion in parallel", done => {
let recurseCount1 = 0;
let recurseCount2 = 0;
const calls: number[] = [];
function go1() {
calls.push(recurseCount1 * 2);
if (++recurseCount1 < maxRecursion) {
Updates.enqueue(go1);
}
}
function go2() {
calls.push(recurseCount2 * 2 + 1);
if (++recurseCount2 < maxRecursion) {
Updates.enqueue(go2);
}
}
Updates.enqueue(go1);
Updates.enqueue(go2);
setTimeout(function () {
expect(calls.length).to.equal(maxRecursion * 2);
for (var index = 0; index < maxRecursion * 2; index++) {
expect(calls[index]).to.equal(index);
}
done();
}, waitMilliseconds);
});
it("throws errors in order without breaking the queue", done => {
const calls: number[] = [];
const dispose = watchSetTimeoutForErrors<number>();
Updates.enqueue(() => {
calls.push(0);
throw 0;
});
Updates.enqueue(() => {
calls.push(1);
throw 1;
});
Updates.enqueue(() => {
calls.push(2);
throw 2;
});
expect(calls).to.be.empty;
setTimeout(() => {
const errors = dispose();
expect(calls).to.eql([0, 1, 2]);
expect(errors).to.eql([0, 1, 2]);
done();
}, waitMilliseconds);
});
it("preserves the respective order of errors interleaved among successes", done => {
const calls: number[] = [];
const dispose = watchSetTimeoutForErrors<number>();
Updates.enqueue(() => {
calls.push(0);
});
Updates.enqueue(() => {
calls.push(1);
throw 1;
});
Updates.enqueue(() => {
calls.push(2);
});
Updates.enqueue(() => {
calls.push(3);
throw 3;
});
Updates.enqueue(() => {
calls.push(4);
throw 4;
});
Updates.enqueue(() => {
calls.push(5);
});
expect(calls).to.be.empty;
setTimeout(() => {
const errors = dispose();
expect(calls).to.eql([0, 1, 2, 3, 4, 5]);
expect(errors).to.eql([1, 3, 4]);
done();
}, waitMilliseconds);
});
it("executes tasks scheduled by another task that later throws an error", done => {
const dispose = watchSetTimeoutForErrors<number>();
Updates.enqueue(() => {
Updates.enqueue(() => {
throw 1;
});
throw 0;
});
setTimeout(() => {
const errors = dispose();
expect(errors).to.eql([0, 1]);
done();
}, waitMilliseconds);
});
it("executes a tree of tasks in breadth-first order when some tasks throw errors", done => {
const calls: number[] = [];
const dispose = watchSetTimeoutForErrors<number>();
Updates.enqueue(() => {
calls.push(0);
Updates.enqueue(() => {
calls.push(2);
Updates.enqueue(() => {
calls.push(5);
throw 5;
});
Updates.enqueue(() => {
calls.push(6);
});
});
Updates.enqueue(() => {
calls.push(3);
});
throw 0;
});
Updates.enqueue(() => {
calls.push(1);
Updates.enqueue(() => {
calls.push(4);
throw 4;
});
});
expect(calls).to.eql([]);
setTimeout(() => {
const errors = dispose();
expect(calls).to.eql([0, 1, 2, 3, 4, 5, 6]);
expect(errors).to.eql([0, 4, 5]);
done();
}, waitMilliseconds);
});
it("rethrows task errors and preserves the order of recursive tasks", done => {
let recursionCount = 0;
const dispose = watchSetTimeoutForErrors<number>();
function go() {
if (++recursionCount < maxRecursion) {
Updates.enqueue(go);
throw recursionCount - 1;
}
}
Updates.enqueue(go);
setTimeout(function () {
const errors = dispose();
expect(recursionCount).to.equal(maxRecursion);
expect(errors.length).to.equal(maxRecursion - 1);
for (let index = 0; index < maxRecursion - 1; index++) {
expect(errors[index]).to.equal(index);
}
done();
}, waitMilliseconds);
});
it("can execute three parallel deep recursions in order, one of which throwing every task", done => {
const dispose = watchSetTimeoutForErrors<number>();
let recurseCount1 = 0;
let recurseCount2 = 0;
let recurseCount3 = 0;
let calls: number[] = [];
function go1() {
calls.push(recurseCount1 * 3);
if (++recurseCount1 < maxRecursion) {
Updates.enqueue(go1);
}
}
function go2() {
calls.push(recurseCount2 * 3 + 1);
if (++recurseCount2 < maxRecursion) {
Updates.enqueue(go2);
}
}
function go3() {
calls.push(recurseCount3 * 3 + 2);
if (++recurseCount3 < maxRecursion) {
Updates.enqueue(go3);
throw recurseCount3 - 1;
}
}
Updates.enqueue(go1);
Updates.enqueue(go2);
Updates.enqueue(go3);
setTimeout(function () {
const errors = dispose();
expect(calls.length).to.equal(maxRecursion * 3);
for (var index = 0; index < maxRecursion * 3; index++) {
expect(calls[index]).to.equal(index);
}
expect(errors.length).to.equal(maxRecursion - 1);
for (var index = 0; index < maxRecursion - 1; index++) {
expect(errors[index]).to.equal(index);
}
done();
}, waitMilliseconds);
});
});
context("when updating DOM synchronously", () => {
beforeEach(() => {
Updates.setMode(false);
});
afterEach(() => {
Updates.setMode(true);
});
it("calls task immediately", () => {
let called = false;
Updates.enqueue(() => {
called = true;
});
expect(called).to.equal(true);
});
it("calls task.call method immediately", () => {
let called = false;
Updates.enqueue({
call: () => {
called = true;
}
});
expect(called).to.equal(true);
});
it("calls multiple tasks in order", () => {
const calls:number[] = [];
Updates.enqueue(() => {
calls.push(0);
});
Updates.enqueue(() => {
calls.push(1);
});
Updates.enqueue(() => {
calls.push(2);
});
expect(calls).to.eql([0, 1, 2]);
});
it("can schedule tasks recursively", () => {
const steps: number[] = [];
Updates.enqueue(() => {
steps.push(0);
Updates.enqueue(() => {
steps.push(2);
Updates.enqueue(() => {
steps.push(4);
});
steps.push(3);
});
steps.push(1);
});
expect(steps).to.eql([0, 1, 2, 3, 4]);
});
it(`can recurse ${maxRecursion} tasks deep`, () => {
let recurseCount = 0;
function go() {
if (++recurseCount < maxRecursion) {
Updates.enqueue(go);
}
}
Updates.enqueue(go);
expect(recurseCount).to.equal(maxRecursion);
});
it("throws errors immediately", () => {
const calls: number[] = [];
let caught: any;
try {
Updates.enqueue(() => {
calls.push(0);
throw 0;
});
} catch(error) {
caught = error;
}
expect(calls).to.eql([0]);
expect(caught).to.eql(0);
});
});
});