Home Reference Source Test

test/utils_test.js

"use strict";

import chai, { expect } from "chai";

import {
  partition,
  delay,
  pMap,
  omit,
  qsify,
  checkVersion,
  support,
  capable,
  nobatch,
  parseDataURL,
  extractFileInfo,
  cleanUndefinedProperties,
} from "../src/utils";

chai.should();
chai.config.includeStack = true;

describe("Utils", () => {
  /** @test {partition} */
  describe("#partition", () => {
    it("should chunk array", () => {
      expect(partition([1, 2, 3], 2)).eql([[1, 2], [3]]);
      expect(partition([1, 2, 3], 1)).eql([[1], [2], [3]]);
      expect(partition([1, 2, 3, 4, 5], 3)).eql([[1, 2, 3], [4, 5]]);
      expect(partition([1, 2], 2)).eql([[1, 2]]);
    });

    it("should not chunk array with n<=0", () => {
      expect(partition([1, 2, 3], 0)).eql([1, 2, 3]);
      expect(partition([1, 2, 3], -1)).eql([1, 2, 3]);
    });
  });

  /** @test {delay} */
  describe("#delay", () => {
    it("should delay resolution after the specified amount of time", () => {
      const start = new Date().getTime();
      return delay(10).then(() => {
        expect(new Date().getTime() - start).to.be.at.least(9);
      });
    });
  });

  /** @test {pMap} */
  describe("#pMap", () => {
    it("should map list to aggregated results", () => {
      return pMap([1, 2], x => Promise.resolve(x * 2)).should.become([2, 4]);
    });

    it("should convert sync reducing function to async", () => {
      return pMap([1, 2], x => x * 2).should.become([2, 4]);
    });

    it("should preserve order of entries", () => {
      return pMap([100, 50], x => {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(x);
          }, x);
        });
      }).should.become([100, 50]);
    });

    it("should ensure order of execution", () => {
      const logged = [];
      return pMap([100, 50], x => {
        return new Promise(resolve => {
          setTimeout(() => {
            logged.push(x);
            resolve(x);
          }, x);
        });
      }).then(_ => expect(logged).eql([100, 50]));
    });
  });

  /** @test {omit} */
  describe("#omit", () => {
    it("should omit provided a single key", () => {
      expect(omit({ a: 1, b: 2 }, "a")).eql({ b: 2 });
    });

    it("should omit multiple keys", () => {
      expect(omit({ a: 1, b: 2, c: 3 }, "a", "c")).eql({ b: 2 });
    });

    it("should return source if no key is specified", () => {
      expect(omit({ a: 1, b: 2 })).eql({ a: 1, b: 2 });
    });
  });

  /** @test {qsify} */
  describe("#qsify", () => {
    it("should generate a query string from an object", () => {
      expect(qsify({ a: 1, b: 2 })).eql("a=1&b=2");
    });

    it("should strip out undefined values", () => {
      expect(qsify({ a: undefined, b: 2 })).eql("b=2");
    });

    it("should join comma-separated values", () => {
      expect(qsify({ a: [1, 2], b: 2 })).eql("a=1,2&b=2");
    });

    it("should map boolean as lowercase string", () => {
      expect(qsify({ a: [true, 2], b: false })).eql("a=true,2&b=false");
    });

    it("should escaped values", () => {
      expect(qsify({ a: ["é", "ə"], b: "&" })).eql("a=%C3%A9,%C9%99&b=%26");
    });
  });

  describe("#checkVersion", () => {
    it("should accept a version within provided range", () => {
      checkVersion("1.0", "1.0", "2.0");
      checkVersion("1.10", "1.0", "2.0");
      checkVersion("1.10", "1.9", "2.0");
      checkVersion("2.1", "1.0", "2.2");
      checkVersion("2.1", "1.2", "2.2");
      checkVersion("1.4", "1.4", "2.0");
    });

    it("should not accept a version oustide provided range", () => {
      expect(() => checkVersion("0.9", "1.0", "2.0")).to.Throw(Error);
      expect(() => checkVersion("2.0", "1.0", "2.0")).to.Throw(Error);
      expect(() => checkVersion("2.1", "1.0", "2.0")).to.Throw(Error);
      expect(() => checkVersion("3.9", "1.0", "2.10")).to.Throw(Error);
      expect(() => checkVersion("1.3", "1.4", "2.0")).to.Throw(Error);
    });
  });

  describe("@support", () => {
    it("should return a function", () => {
      expect(support()).to.be.a("function");
    });

    it("should make decorated method resolve on version match", () => {
      class FakeClient {
        fetchHTTPApiVersion() {
          return Promise.resolve("1.4"); // simulates a successful checkVersion call
        }

        @support("1.0", "2.0")
        test() {
          return Promise.resolve();
        }
      }

      return new FakeClient().test().should.be.fulfilled;
    });

    it("should make decorated method rejecting on version mismatch", () => {
      class FakeClient {
        fetchHTTPApiVersion() {
          return Promise.resolve("1.4"); // simulates a failing checkVersion call
        }

        @support("1.5", "2.0")
        test() {
          return Promise.resolve();
        }
      }

      return new FakeClient().test().should.be.rejected;
    });

    it("should check for an attached client instance", () => {
      class FakeClient {
        constructor() {
          this.client = {
            fetchHTTPApiVersion() {
              return Promise.reject(); // simulates a failing checkVersion call
            },
          };
        }

        @support()
        test() {
          return Promise.resolve();
        }
      }

      return new FakeClient().test().should.be.rejected;
    });
  });

  describe("@capable", () => {
    it("should return a function", () => {
      expect(capable()).to.be.a("function");
    });

    it("should make decorated method checking the capabilities", () => {
      class FakeClient {
        fetchServerCapabilities() {
          return Promise.resolve({}); // simulates a successful checkVersion call
        }

        @capable([])
        test() {
          return Promise.resolve();
        }
      }

      return new FakeClient().test().should.be.fulfilled;
    });

    it("should make decorated method resolve on capability match", () => {
      class FakeClient {
        fetchServerCapabilities() {
          return Promise.resolve({
            attachments: {},
            default: {},
            "auth:fxa": {},
          });
        }

        @capable(["default", "attachments"])
        test() {
          return Promise.resolve();
        }
      }

      return new FakeClient().test().should.be.fulfilled;
    });

    it("should make decorated method rejecting on missing capability", () => {
      class FakeClient {
        fetchServerCapabilities() {
          return Promise.resolve({ attachments: {} });
        }

        @capable(["attachments", "default"])
        test() {
          return Promise.resolve();
        }
      }

      return new FakeClient()
        .test()
        .should.be.rejectedWith(Error, /default not present/);
    });
  });

  describe("@nobatch", () => {
    it("should return a function", () => {
      expect(nobatch()).to.be.a("function");
    });

    it("should make decorated method pass when not in batch", () => {
      class FakeClient {
        constructor() {
          this._isBatch = false;
        }

        @nobatch("error")
        test() {
          return Promise.resolve();
        }
      }

      return new FakeClient().test().should.be.fulfilled;
    });

    it("should make decorated method to throw if in batch", () => {
      class FakeClient {
        constructor() {
          this._isBatch = true;
        }

        @nobatch("error")
        test() {
          return Promise.resolve();
        }
      }

      expect(() => new FakeClient().test()).to.Throw(Error, "error");
    });
  });

  describe("parseDataURL()", () => {
    it("should extract expected properties", () => {
      expect(
        parseDataURL("data:image/png;encoding=utf-8;name=a.png;base64,b64")
      ).eql({
        type: "image/png",
        name: "a.png",
        base64: "b64",
        encoding: "utf-8",
      });
    });

    it("should support dataURL without name", () => {
      expect(parseDataURL("data:image/png;base64,b64")).eql({
        type: "image/png",
        base64: "b64",
      });
    });

    it("should throw an error when the data url is invalid", () => {
      expect(() => expect(parseDataURL("gni"))).to.throw(
        Error,
        "Invalid data-url: gni..."
      );
    });
  });

  describe("extractFileInfo()", () => {
    it("should extract file information from a data url", () => {
      const dataURL = "data:text/plain;name=t.txt;base64," + btoa("test");

      const { blob, name } = extractFileInfo(dataURL);

      expect(blob.length).eql(4);
      expect(name).eql("t.txt");
    });
  });

  describe("cleanUndefinedProperties()", () => {
    it("should remove undefined properties from an object", () => {
      const obj1 = cleanUndefinedProperties({ a: 1, b: undefined });
      expect(obj1.hasOwnProperty("a")).eql(true);
      expect(obj1.hasOwnProperty("b")).eql(false);
    });
  });
});