import {delay} from 'app/common/delay'; import * as gutil from 'app/common/gutil'; import {assert} from 'chai'; import {Observable} from 'grainjs'; import * as ko from 'knockout'; import * as sinon from 'sinon'; describe('gutil2', function() { describe('waitObs', function() { it('should resolve promise when predicate matches', async function() { const obs: ko.Observable = ko.observable(null); const promise1 = gutil.waitObs(obs, (val) => Boolean(val)); const promise2 = gutil.waitObs(obs, (val) => (val === null)); const promise3 = gutil.waitObs(obs, (val) => (val! > 20)); const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy(); const done = Promise.all([ promise1.then((val) => { spy1(); assert.strictEqual(val, 17); }), promise2.then((val) => { spy2(); assert.strictEqual(val, null); }), promise3.then((val) => { spy3(); assert.strictEqual(val, 30); }), ]); await delay(1); obs(17); await delay(1); obs(30); await delay(1); await done; sinon.assert.callOrder(spy2, spy1, spy3); }); }); describe('waitGrainObs', function() { it('should resolve promise when predicate matches', async function() { const obs = Observable.create(null, null); const promise1 = gutil.waitGrainObs(obs, (val) => Boolean(val)); const promise2 = gutil.waitGrainObs(obs, (val) => (val === null)); const promise3 = gutil.waitGrainObs(obs, (val) => (val! > 20)); const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy(); const done = Promise.all([ promise1.then((val) => { spy1(); assert.strictEqual(val, 17); }), promise2.then((val) => { spy2(); assert.strictEqual(val, null); }), promise3.then((val) => { spy3(); assert.strictEqual(val, 30); }), ]); await delay(1); obs.set(17); await delay(1); obs.set(30); await delay(1); await done; sinon.assert.callOrder(spy2, spy1, spy3); }); }); describe('PromiseChain', function() { it('should resolve promises in order', async function() { const chain = new gutil.PromiseChain(); const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy(); const done = Promise.all([ chain.add(() => delay(30).then(spy1).then(() => 1)), chain.add(() => delay(20).then(spy2).then(() => 2)), chain.add(() => delay(10).then(spy3).then(() => 3)), ]); assert.deepEqual(await done, [1, 2, 3]); sinon.assert.callOrder(spy1, spy2, spy3); }); it('should skip pending callbacks, but not new callbacks, on error', async function() { const chain = new gutil.PromiseChain(); const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy(); let res1: any, res2: any, res3: any; await assert.isRejected(Promise.all([ res1 = chain.add(() => delay(30).then(spy1).then(() => { throw new Error('Err1'); })), res2 = chain.add(() => delay(20).then(spy2)), res3 = chain.add(() => delay(10).then(spy3)), ]), /Err1/); // Check that already-scheduled callbacks did not get called. sinon.assert.calledOnce(spy1); sinon.assert.notCalled(spy2); sinon.assert.notCalled(spy3); spy1.resetHistory(); // Ensure skipped add() calls return a rejection. await assert.isRejected(res1, /^Err1/); await assert.isRejected(res2, /^Skipped due to an earlier error/); await assert.isRejected(res3, /^Skipped due to an earlier error/); // New promises do get scheduled. await assert.isRejected(Promise.all([ res1 = chain.add(() => delay(1).then(spy1).then(() => 17)), res2 = chain.add(() => delay(1).then(spy2).then(() => { throw new Error('Err2'); })), res3 = chain.add(() => delay(1).then(spy3)), ]), /Err2/); sinon.assert.callOrder(spy1, spy2); sinon.assert.notCalled(spy3); // Check the return values of add() calls. assert.strictEqual(await res1, 17); await assert.isRejected(res2, /^Err2/); await assert.isRejected(res3, /^Skipped due to an earlier error/); }); }); describe("isLongerThan", function() { it('should work correctly', async function() { assert.equal(await gutil.isLongerThan(delay(200), 100), true); assert.equal(await gutil.isLongerThan(delay(10), 100), false); // A promise that throws before the timeout, causes the returned promise to resolve to false. const errorObj = {}; let promise = delay(10).then(() => { throw errorObj; }); assert.equal(await gutil.isLongerThan(promise, 100), false); await assert.isRejected(promise); // A promise that throws after the timeout, causes the returned promise to resolve to true. promise = delay(200).then(() => { throw errorObj; }); assert.equal(await gutil.isLongerThan(promise, 100), true); await assert.isRejected(promise); }); }); describe("timeoutReached", function() { const DELAY_1 = 20; const DELAY_2 = 2 * DELAY_1; it("should return true for timed out promise", async function() { assert.isTrue(await gutil.timeoutReached(DELAY_1, delay(DELAY_2))); assert.isTrue(await gutil.timeoutReached(DELAY_1, delay(DELAY_2).then(() => { throw new Error("test error"); }))); }); it("should return false for promise that completes before timeout", async function() { assert.isFalse(await gutil.timeoutReached(DELAY_2, delay(DELAY_1))); assert.isFalse(await gutil.timeoutReached(DELAY_2, delay(DELAY_1) .then(() => { throw new Error("test error"); }))); assert.isFalse(await gutil.timeoutReached(DELAY_2, Promise.resolve('foo'))); assert.isFalse(await gutil.timeoutReached(DELAY_2, Promise.reject('bar'))); }); }); describe("isValidHex", function() { it('should work correctly', async function() { assert.equal(gutil.isValidHex('#FF00FF'), true); assert.equal(gutil.isValidHex('#FF00FFF'), false); assert.equal(gutil.isValidHex('#FF0'), false); assert.equal(gutil.isValidHex('#FF00'), false); assert.equal(gutil.isValidHex('FF00FF'), false); assert.equal(gutil.isValidHex('#FF00FG'), false); }); }); describe("pruneArray", function() { function check(arr: T[], indexes: number[], expect: T[]) { gutil.pruneArray(arr, indexes); assert.deepEqual(arr, expect); } it('should remove correct elements', function() { check(['a', 'b', 'c'], [], ['a', 'b', 'c']); check(['a', 'b', 'c'], [0], ['b', 'c']); check(['a', 'b', 'c'], [1], ['a', 'c']); check(['a', 'b', 'c'], [2], ['a', 'b']); check(['a', 'b', 'c'], [0, 1], ['c']); check(['a', 'b', 'c'], [0, 2], ['b']); check(['a', 'b', 'c'], [1, 2], ['a']); check(['a', 'b', 'c'], [0, 1, 2], []); check([], [], []); check(['a'], [], ['a']); check(['a'], [0], []); }); }); });