diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..539ba93
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+  - package-ecosystem: "npm"
+    directory: "/"
+    schedule:
+      interval: "weekly"
\ No newline at end of file
diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml
new file mode 100644
index 0000000..583ce73
--- /dev/null
+++ b/.github/workflows/tester.yml
@@ -0,0 +1,68 @@
+name: Tester
+
+on:
+  push:
+    branches:
+      - "main"
+    paths:
+      - "src/**"
+      - "test/**"
+      - "package.json"
+      - "tsconfig.json"
+      - ".github/workflows/tester.yml"
+  pull_request:
+    paths:
+      - "src/**"
+      - "test/**"
+      - "package.json"
+      - "tsconfig.json"
+      - ".github/workflows/tester.yml"
+
+permissions:
+  contents: read
+
+jobs:
+  tester:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        node-version: ["18.x"]
+      fail-fast: false
+    steps:
+      - uses: actions/checkout@v4
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Install Dependencies
+        run: npm install
+      - name: Test
+        run: npm test
+        env:
+          CI: true
+  coverage:
+    permissions:
+      checks: write # for coverallsapp/github-action to create new checks
+      contents: read # for actions/checkout to fetch code
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        node-version: ["18.x"]
+    steps:
+      - uses: actions/checkout@v4
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Install Dependencies
+        run: npm install
+      - name: Coverage
+        run: npm run test-cov
+        env:
+          CI: true
+      - name: Coveralls
+        uses: coverallsapp/github-action@master
+        with:
+          github-token: ${{ github.token }}
\ No newline at end of file
diff --git a/package.json b/package.json
index 9a76e46..87830a8 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,9 @@
   "browser": "dist/index.umd.js",
   "types": "dist/index.d.ts",
   "scripts": {
-    "build": "rollup -c rollup.config.mjs"
+    "build": "rollup -c rollup.config.mjs",
+    "test": "mocha -r ts-node/register 'test/**/*.ts'",
+    "test-cov": "c8 --reporter=html --reporter=text-summary npm test"
   },
   "repository": {
     "type": "git",
@@ -19,7 +21,16 @@
     "@rollup/plugin-commonjs": "^25.0.7",
     "@rollup/plugin-terser": "^0.4.4",
     "@rollup/plugin-typescript": "^11.1.5",
+    "@types/chai": "^4.3.16",
+    "@types/jsdom": "^21.1.7",
+    "@types/mocha": "^10.0.7",
+    "c8": "^10.1.2",
+    "chai": "4",
+    "jsdom": "^24.1.0",
+    "mocha": "^10.6.0",
     "rollup": "^4.9.4",
+    "ts-node": "^10.9.2",
+    "tslib": "^2.6.3",
     "typescript": "^5.3.3"
   },
   "keywords": [
diff --git a/src/events/events.ts b/src/events/events.ts
index 87c2df4..9e8fd60 100644
--- a/src/events/events.ts
+++ b/src/events/events.ts
@@ -28,6 +28,25 @@ export function on(
   });
 }
 
+export function off(
+  els:
+    | Window
+    | Document
+    | HTMLElement
+    | NodeList
+    | Array<HTMLElement>
+    | HTMLCollection,
+  events: string | string[],
+  listener: EventListener,
+  useCapture?: boolean
+): void {
+  eventForEach(events, (e) => {
+    forEachEls(els, (el) => {
+      el.removeEventListener(e, listener, useCapture);
+    });
+  });
+}
+
 // do not support IE !!!
 export function trigger(
   els:
@@ -41,10 +60,12 @@ export function trigger(
   opts: Partial<PjaxOptions> = {}
 ): void {
   eventForEach(events, (e) => {
-    const event = new CustomEvent(e, {
+    const event = new Event(e, {
       bubbles: true,
       cancelable: true,
-      ...opts,
+    });
+    Object.keys(opts).forEach((key) => {
+      event[key] = opts[key];
     });
     forEachEls(els, (el) => {
       el.dispatchEvent(event);
diff --git a/src/forEachSelectors.ts b/src/forEachSelectors.ts
index cf64ece..15ea586 100644
--- a/src/forEachSelectors.ts
+++ b/src/forEachSelectors.ts
@@ -3,8 +3,8 @@ import forEachEls from "./forEachEls";
 export default function (
   selectors: string[],
   cb: (el: HTMLElement, index: number, array: HTMLElement[]) => void,
-  context: any,
-  DOMcontext: Document = document
+  context?: any,
+  DOMcontext: HTMLElement | Document = document
 ): void {
   selectors.forEach((selector) => {
     forEachEls(DOMcontext.querySelectorAll(selector), cb, context);
diff --git a/src/sendRequest.ts b/src/sendRequest.ts
index 157396d..72c8a86 100644
--- a/src/sendRequest.ts
+++ b/src/sendRequest.ts
@@ -9,7 +9,7 @@ export default function (
     request: XMLHttpRequest,
     location: string,
     options: Partial<PjaxOptions>
-  ) => XMLHttpRequest
+  ) => any
 ): XMLHttpRequest {
   const requestOptions = options.requestOptions || {};
   const requestMethod = (requestOptions.requestMethod || "GET").toUpperCase();
diff --git a/src/util/contains.ts b/src/util/contains.ts
index e2cfd0b..8b469cf 100644
--- a/src/util/contains.ts
+++ b/src/util/contains.ts
@@ -3,14 +3,9 @@ export default function (
   selectors: string[],
   el: Element
 ): boolean {
-  for (const selector of selectors) {
-    const selectedEls = doc.querySelectorAll(selector);
-    for (let j = 0; j < selectedEls.length; j++) {
-      if (selectedEls[j].contains(el)) {
-        return true;
-      }
-    }
-  }
-
-  return false;
+  return selectors.some(selector =>
+    Array.from(doc.querySelectorAll(selector)).some(selectedEl =>
+      selectedEl.contains(el)
+    )
+  );
 }
diff --git a/test/evalScript.ts b/test/evalScript.ts
new file mode 100644
index 0000000..b3daa17
--- /dev/null
+++ b/test/evalScript.ts
@@ -0,0 +1,40 @@
+import chai from "chai";
+import "./jsdom";
+import evalScript from "../src/evalScript";
+
+const should = chai.should();
+
+describe("evalScript", () => {
+  it("test evalScript method", () => {
+    document.body.className = "";
+    const script = document.createElement("script");
+    script.innerHTML = "document.body.className = 'executed'";
+
+    document.body.className.should.equal("");
+    evalScript(script);
+    document.body.className.should.equal("executed");
+
+    script.innerHTML = "document.write('failure')";
+
+    const bodyText = "document.write hasn't been executed";
+    // @ts-expect-error
+    document.body.text = bodyText;
+    evalScript(script);
+    // @ts-expect-error
+    document.body.text.should.equal(bodyText);
+  });
+
+  it("evalScript should not throw an error if the script removed itself", () => {
+    const script = document.createElement("script");
+    script.id = "myScript";
+    script.innerHTML =
+      "const script = document.querySelector('#myScript');" +
+      "script.parentNode.removeChild(script);";
+
+    try {
+      evalScript(script);
+    } catch (e) {
+      should.fail("evalScript should not throw an error if the script removed itself");
+    }
+  });
+});
diff --git a/test/executeScripts.ts b/test/executeScripts.ts
new file mode 100644
index 0000000..7de9b1d
--- /dev/null
+++ b/test/executeScripts.ts
@@ -0,0 +1,29 @@
+import chai from "chai";
+import "./jsdom";
+import executeScripts from "../src/executeScripts";
+
+const should = chai.should();
+
+describe("executeScripts", () => {
+  it("test executeScripts method when the script tag is inside a container", () => {
+    document.body.className = "";
+
+    const container = document.createElement("div");
+    container.innerHTML = `
+    <script>document.body.className = 'executed';</script>
+    <script>document.body.className += ' correctly';</script>`;
+    document.body.className.should.equal("");
+    executeScripts(container);
+    document.body.className.should.equal("executed correctly");
+  });
+
+  it("test executeScripts method with just a script tag", () => {
+    document.body.className = "";
+
+    const script = document.createElement("script");
+    script.innerHTML = "document.body.className = 'executed correctly';";
+    document.body.className.should.equal("");
+    executeScripts(script);
+    document.body.className.should.equal("executed correctly");
+  });
+});
diff --git a/test/forEachEls.ts b/test/forEachEls.ts
new file mode 100644
index 0000000..285802d
--- /dev/null
+++ b/test/forEachEls.ts
@@ -0,0 +1,40 @@
+import chai from "chai";
+import "./jsdom";
+import forEachEls from "../src/forEachEls";
+
+const should = chai.should();
+
+const div = document.createElement("div");
+const span = document.createElement("span");
+const cb = (el) => {
+  el.innerHTML = "boom";
+};
+
+describe("forEachEls", () => {
+  it("test forEachEls on one element", () => {
+    div.innerHTML = "div tag";
+    forEachEls(div, cb);
+    div.innerHTML.should.equal("boom");
+  });
+
+  it("test forEachEls on an array", () => {
+    div.innerHTML = "div tag";
+    span.innerHTML = "span tag";
+
+    forEachEls([div, span], cb);
+    div.innerHTML.should.equal("boom");
+    span.innerHTML.should.equal("boom");
+  });
+
+  it("test forEachEls on a NodeList", () => {
+    div.innerHTML = "div tag";
+    span.innerHTML = "span tag";
+
+    const frag = document.createDocumentFragment();
+    frag.appendChild(div);
+    frag.appendChild(span);
+    forEachEls(frag.childNodes, cb);
+    div.innerHTML.should.equal("boom");
+    span.innerHTML.should.equal("boom");
+  });
+});
diff --git a/test/forEachSelectors.ts b/test/forEachSelectors.ts
new file mode 100644
index 0000000..88c2e67
--- /dev/null
+++ b/test/forEachSelectors.ts
@@ -0,0 +1,25 @@
+import chai from "chai";
+import "./jsdom";
+import forEachSelectors from "../src/forEachSelectors";
+
+const should = chai.should();
+
+const cb =  (el) =>{
+  el.className = "modified";
+};
+
+describe("forEachSelectors", () => {
+  it("test forEachSelector", () => {
+    forEachSelectors(["html", "body"], cb);
+    document.documentElement.className.should.equal("modified");
+    document.body.className.should.equal("modified");
+
+    document.documentElement.className = "";
+    document.body.className = "";
+
+    forEachSelectors(["html", "body"], cb, null, document.documentElement);
+
+    document.documentElement.className.should.equal("");
+    document.body.className.should.equal("modified");
+  });
+});
diff --git a/test/jsdom.ts b/test/jsdom.ts
new file mode 100644
index 0000000..00cda77
--- /dev/null
+++ b/test/jsdom.ts
@@ -0,0 +1,25 @@
+import jsdom from "jsdom";
+
+const { JSDOM } = jsdom;
+
+const dom = new JSDOM("", {
+  url: "https://example.org/",
+  runScripts: "dangerously",
+});
+
+// @ts-ignore
+globalThis.window = dom.window;
+globalThis.document = dom.window.document;
+globalThis.navigator = dom.window.navigator;
+globalThis.location = dom.window.location;
+globalThis.Element = dom.window.Element;
+globalThis.HTMLElement = dom.window.HTMLElement;
+globalThis.HTMLScriptElement = dom.window.HTMLScriptElement;
+globalThis.HTMLHeadElement = dom.window.HTMLHeadElement;
+globalThis.HTMLBodyElement = dom.window.HTMLBodyElement;
+globalThis.Node = dom.window.Node;
+globalThis.HTMLCollection = dom.window.HTMLCollection;
+globalThis.NodeList = dom.window.NodeList;
+globalThis.XMLHttpRequest = dom.window.XMLHttpRequest;
+globalThis.CustomEvent = dom.window.CustomEvent;
+globalThis.Event = dom.window.Event;
\ No newline at end of file
diff --git a/test/newUid.ts b/test/newUid.ts
new file mode 100644
index 0000000..2e51286
--- /dev/null
+++ b/test/newUid.ts
@@ -0,0 +1,12 @@
+import chai from "chai";
+import newUid from "../src/newUid";
+
+const should = chai.should();
+
+describe("newUid", () => {
+  it("test uniqueid", () => {
+    const a = newUid();
+    const b = newUid();
+    a.should.not.equal(b);
+  });
+});
diff --git a/test/parseOptions.ts b/test/parseOptions.ts
new file mode 100644
index 0000000..4caab29
--- /dev/null
+++ b/test/parseOptions.ts
@@ -0,0 +1,36 @@
+import chai from "chai";
+import "./jsdom";
+import parseOptions from "../src/parseOptions";
+
+const should = chai.should();
+
+describe("parseOptions", () => {
+  it('test parse initialization options function - default options', () => {
+    const pjax = {
+      options: parseOptions({})
+    };
+    pjax.options.elements.should.equal("a[href]");
+    pjax.options.selectors.length.should.equal(2);
+    pjax.options.selectors[0].should.equal("title");
+    pjax.options.selectors[1].should.equal(".js-Pjax");
+
+    pjax.options.switches.should.be.an("object");
+    Object.keys(pjax.options.switches).length.should.equal(2);
+
+    pjax.options.switchesOptions.should.be.an("object");
+    Object.keys(pjax.options.switchesOptions).length.should.equal(0);
+
+    pjax.options.history.should.equal(true);
+    pjax.options.scrollTo.should.equal(0);
+    pjax.options.scrollRestoration.should.equal(true);
+    pjax.options.cacheBust.should.equal(true);
+    pjax.options.currentUrlFullReload.should.equal(false);
+  });
+
+  it('test parse initialization options function - scrollTo remains false', () => {
+    const pjax = {
+      options: parseOptions({ scrollTo: false })
+    };
+    pjax.options.scrollTo.should.equal(false);
+  });
+});
\ No newline at end of file
diff --git a/test/proto/attachLink.ts b/test/proto/attachLink.ts
new file mode 100644
index 0000000..c91a27c
--- /dev/null
+++ b/test/proto/attachLink.ts
@@ -0,0 +1,151 @@
+import chai from "chai";
+import "../jsdom";
+import attachLink from "../../src/proto/attachLink";
+import { on, trigger } from "../../src/events/events";
+
+const should = chai.should();
+
+const attr = "data-pjax-state";
+
+describe("attachLink", () => {
+  it("test attachLink method", () => {
+    const a = document.createElement("a");
+    let loadUrlCalled = false;
+
+    attachLink.call(
+      {
+        options: {},
+        loadUrl: () => {
+          loadUrlCalled = true;
+        },
+      },
+      a
+    );
+
+    const internalUri =
+      window.location.protocol +
+      "//" +
+      window.location.host +
+      window.location.pathname +
+      window.location.search;
+
+    a.href = internalUri;
+    trigger(a, "click", { metaKey: true });
+    "modifier".should.equal(a.getAttribute(attr));
+
+    a.href = "http://external.com/";
+    trigger(a, "click");
+    "external".should.equal(a.getAttribute(attr));
+
+    window.location.hash = "#anchor";
+    a.href = internalUri + "#anchor";
+    trigger(a, "click");
+    "anchor".should.equal(a.getAttribute(attr));
+
+    a.href = internalUri + "#another-anchor";
+    trigger(a, "click");
+    "anchor".should.equal(a.getAttribute(attr));
+    window.location.hash = "";
+
+    a.href = internalUri + "#";
+    trigger(a, "click");
+    "anchor-empty".should.equal(a.getAttribute(attr));
+
+    a.href =
+      window.location.protocol + "//" + window.location.host + "/internal";
+    trigger(a, "click");
+    "load".should.equal(a.getAttribute(attr));
+    loadUrlCalled.should.equal(true);
+  });
+
+  it("test attach link preventDefaulted events", () => {
+    let loadUrlCalled = false;
+    const a = document.createElement("a");
+
+    // This needs to be before the call to attachLink()
+    on(a, "click", (event) => {
+      event.preventDefault();
+    });
+
+    attachLink.call(
+      {
+        options: {},
+        loadUrl: () => {
+          loadUrlCalled = true;
+        },
+      },
+      a
+    );
+
+    a.href = "#";
+    trigger(a, "click");
+
+    loadUrlCalled.should.equal(false);
+  });
+
+  it("test options are not modified by attachLink", () => {
+    const a = document.createElement("a");
+    const options = { foo: "bar" };
+    const loadUrl = () => {};
+
+    attachLink.call({ options: options, loadUrl: loadUrl }, a);
+
+    a.href =
+      window.location.protocol +
+      "//" +
+      window.location.host +
+      window.location.pathname +
+      window.location.search;
+
+    trigger(a, "click");
+
+    Object.keys(options).length.should.equal(1);
+    options.foo.should.equal("bar");
+  });
+
+  it("test link triggered by keyboard", (done) => {
+    const a = document.createElement("a");
+    const pjax = {
+      options: {},
+      loadUrl: () => {
+        a.getAttribute(attr)!.should.equal("load");
+        done();
+      },
+    };
+
+    attachLink.call(pjax, a);
+
+    a.href =
+      window.location.protocol + "//" + window.location.host + "/internal";
+
+    trigger(a, "keyup", { keyCode: 14 });
+    a.getAttribute(attr)!.should.equal("");
+
+    trigger(a, "keyup", { keyCode: 13, metaKey: true });
+    a.getAttribute(attr)!.should.equal("modifier");
+
+    trigger(a, "keyup", { keyCode: 13 });
+  });
+
+  it("test link with the same URL as the current one, when currentUrlFullReload set to true", (done) => {
+    window.location.href = "https://example.org/";
+    const a = document.createElement("a");
+    const pjax = {
+      options: {
+        currentUrlFullReload: true,
+      },
+      reload: () => {
+        done();
+      },
+      loadUrl: () => {
+        should.fail("loadUrl() was called wrongly");
+        done();
+      },
+    };
+
+    attachLink.call(pjax, a);
+    a.href = window.location.href;
+    trigger(a, "click");
+    a.getAttribute(attr)!.should.equal("reload");
+  });
+});
diff --git a/test/proto/handleResponse.ts b/test/proto/handleResponse.ts
new file mode 100644
index 0000000..17be6c0
--- /dev/null
+++ b/test/proto/handleResponse.ts
@@ -0,0 +1,196 @@
+import chai from "chai";
+import "../jsdom";
+import handleResponse from "../../src/proto/handleResponse";
+import Pjax from "../../src";
+
+const loadContent = Pjax.prototype.loadContent;
+const noop = () => {};
+const should = chai.should();
+
+const href = "https://example.org/";
+
+describe("handleResponse", () => {
+  let storeEventHandler;
+  let pjaxErrorEventTriggerTest;
+
+  it("test events triggered when handleResponse(false) is called", (done) => {
+    const pjax = {
+      options: {
+        test: 1,
+      },
+    };
+
+    const events: string[] = [];
+
+    let i = 0;
+
+    storeEventHandler = (e) => {
+      events.push(e.type as string);
+      e.test.should.equal(1);
+      i++;
+      if (i === 2) {
+        done();
+      }
+    };
+
+    document.addEventListener("pjax:complete", storeEventHandler);
+    document.addEventListener("pjax:error", storeEventHandler);
+
+    handleResponse.bind(pjax)(false, null);
+
+    events.should.deep.equal(["pjax:complete", "pjax:error"]);
+
+    document.removeEventListener("pjax:complete", storeEventHandler);
+    document.removeEventListener("pjax:error", storeEventHandler);
+  });
+
+  it("test when handleResponse() is called normally", () => {
+    const pjax = {
+      options: {
+        test: 1,
+      },
+      loadContent: noop,
+      state: {} as any,
+    };
+
+    const request = {
+      getResponseHeader: noop,
+    };
+
+    handleResponse.bind(pjax)("", request, "href");
+
+    delete window.history.state.uid;
+
+    window.history.state.should.deep.equal({
+      url: href,
+      title: "",
+      scrollPos: [0, 0],
+    });
+    pjax.state.href.should.equal("href");
+    Object.keys(pjax.state.options).length.should.equal(2);
+    pjax.state.options.request.should.equal(request);
+  });
+
+  it("test when handleResponse() is called normally with request.responseURL", () => {
+    const pjax = {
+      options: {},
+      loadContent: noop,
+      state: {} as any,
+    };
+
+    const request = {
+      responseURL: href + "1",
+      getResponseHeader: noop,
+    };
+
+    handleResponse.bind(pjax)("", request, "");
+
+    pjax.state.href.should.equal(request.responseURL);
+  });
+
+  it("test when handleResponse() is called normally with X-PJAX-URL", () => {
+    const pjax = {
+      options: {},
+      loadContent: noop,
+      state: {} as any,
+    };
+
+    const request = {
+      getResponseHeader: (header) => {
+        if (header === "X-PJAX-URL") {
+          return href + "2";
+        }
+      },
+    };
+
+    handleResponse.bind(pjax)("", request, "");
+
+    pjax.state.href.should.equal(href + "2");
+  });
+
+  it("test when handleResponse() is called normally with X-XHR-Redirected-To", () => {
+    const pjax = {
+      options: {},
+      loadContent: noop,
+      state: {} as any,
+    };
+
+    const request = {
+      getResponseHeader: (header) => {
+        if (header === "X-XHR-Redirected-To") {
+          return href + "3";
+        }
+      },
+    };
+
+    handleResponse.bind(pjax)("", request, "");
+
+    pjax.state.href.should.equal(href + "3");
+  });
+
+  it("test when handleResponse() is called normally with a hash", () => {
+    const pjax = {
+      options: {},
+      loadContent: noop,
+      state: {} as any,
+    };
+
+    const request = {
+      responseURL: href + "2",
+      getResponseHeader: noop,
+    };
+
+    handleResponse.bind(pjax)("", request, href + "1#test");
+
+    pjax.state.href.should.equal(href + "2#test");
+  });
+
+  it("test try...catch for loadContent()", () => {
+    const pjax = {
+      options: {},
+      loadContent: () => {
+        throw new Error();
+      },
+      latestChance: () => true,
+      state: {},
+    };
+
+    const request = {
+      getResponseHeader: noop,
+    };
+
+    document.removeEventListener("pjax:error", pjaxErrorEventTriggerTest);
+
+    should.not.throw(() => {
+      handleResponse.bind(pjax)("", request, "").should.equal(true);
+    });
+  });
+
+  it("test events triggered when loadContent() is called with a non-string html argument", (done) => {
+    const options = {
+      test: 1,
+    };
+
+    const events: string[] = [];
+
+    let i = 0;
+
+    storeEventHandler = (e) => {
+      events.push(e.type);
+      e.test.should.equal(1);
+      i++;
+      if (i === 2) done();
+    };
+
+    document.addEventListener("pjax:complete", storeEventHandler);
+    document.addEventListener("pjax:error", storeEventHandler);
+
+    // @ts-expect-error
+    loadContent(null, options);
+
+    events.should.deep.equal(["pjax:complete", "pjax:error"]);
+
+    document.removeEventListener("pjax:complete", storeEventHandler);
+    document.removeEventListener("pjax:error", storeEventHandler);
+  });
+});
diff --git a/test/proto/parseElement.ts b/test/proto/parseElement.ts
new file mode 100644
index 0000000..6b61ad1
--- /dev/null
+++ b/test/proto/parseElement.ts
@@ -0,0 +1,22 @@
+import chai from "chai";
+import "../jsdom";
+import parseElement from "../../src/proto/parseElement";
+
+const should = chai.should();
+
+const pjax = {
+  attachLink: () => true,
+};
+
+describe("parseElement", () => {
+  it("test parse element prototype method", () => {
+    should.not.throw(() => {
+      const a = document.createElement("a");
+      parseElement.call(pjax, a);
+    });
+    should.throw(() => {
+      const el = document.createElement("div");
+      parseElement.call(pjax, el);
+    });
+  });
+});
diff --git a/test/sendRequest.ts b/test/sendRequest.ts
new file mode 100644
index 0000000..17ff674
--- /dev/null
+++ b/test/sendRequest.ts
@@ -0,0 +1,78 @@
+import chai from "chai";
+import "./jsdom";
+import sendRequest from "../src/sendRequest";
+
+const should = chai.should();
+
+describe("sendRequest", () => {
+  it("test xhr request - request is made, gets a result, and is cache-busted", () => {
+    const url = "https://httpbin.org/get";
+    const r = sendRequest(url, { cacheBust: true }, (result: any) => {
+      r.responseURL.indexOf("?").should.equal(url.length);
+      try {
+        result = JSON.parse(result!);
+      } catch (e) {
+        should.fail("xhr doesn't get a JSON response");
+      }
+      result.should.be.an("object");
+    });
+  });
+  it("test xhr request - request is not cache-busted when configured not to be", () => {
+    const url = "https://httpbin.org/get";
+    const r = sendRequest(url, {}, () => {
+      r.responseURL.should.equal(url);
+    });
+  });
+  it("request headers are sent properly", () => {
+    const url = "https://httpbin.org/headers";
+    const options = {
+      selectors: ["div.pjax", "div.container"],
+    };
+
+    sendRequest(url, options, (responseText) => {
+      const headers = JSON.parse(responseText!).headers;
+
+      headers["X-Requested-With"].should.equal("XMLHttpRequest");
+      headers["X-Pjax"].should.equal("true");
+      headers["X-Pjax-Selectors"].should.equal('["div.pjax","div.container"]');
+    });
+  });
+  it("HTTP status codes other than 200 are handled properly", () => {
+    const url = "https://httpbin.org/status/400";
+
+    sendRequest(url, {}, (responseText, request) => {
+      should.equal(responseText, null);
+      request.status.should.equal(400);
+    });
+  });
+  it("GET query data is sent properly", () => {
+    const url = "https://httpbin.org/get";
+    const params = [
+      {
+        name: "test",
+        value: "1",
+      },
+    ];
+    const options = {
+      requestOptions: {
+        requestParams: params,
+      },
+    };
+
+    sendRequest(url, options, (responseText) => {
+      const response = JSON.parse(responseText!);
+      response.args[params[0].name].should.equal(params[0].value);
+    });
+  });
+
+  it("XHR timeout is handled properly", () => {
+    const url = "https://httpbin.org/delay/5";
+    const options = {
+      timeout: 1000,
+    };
+
+    sendRequest(url, options, (responseText) => {
+      should.equal(responseText, null);
+    });
+  });
+});
diff --git a/test/switches.ts b/test/switches.ts
new file mode 100644
index 0000000..863b9f2
--- /dev/null
+++ b/test/switches.ts
@@ -0,0 +1,55 @@
+import chai from "chai";
+import "./jsdom";
+import { innerHTML, outerHTML } from "../src/switches";
+
+const should = chai.should();
+
+const noop = () => {};
+
+describe("switches", () => {
+  it("test outerHTML switch", () => {
+    const doc = document.implementation.createHTMLDocument();
+
+    const container = doc.createElement("div");
+    container.innerHTML = "<p id='p'>Original Text</p>";
+    doc.body.appendChild(container);
+
+    const p = doc.createElement("p");
+    p.innerHTML = "New Text";
+
+    outerHTML.bind({
+      onSwitch: noop,
+    })(doc.querySelector("p"), p);
+
+    doc.querySelector("p")!.innerHTML.should.equal("New Text");
+    doc.querySelector("p")!.id.should.not.equal("p");
+  });
+
+  it("test innerHTML switch", () => {
+    const doc = document.implementation.createHTMLDocument();
+
+    const container = doc.createElement("div");
+    container.innerHTML = "<p id='p'>Original Text</p>";
+    doc.body.appendChild(container);
+
+    const p = doc.createElement("p");
+    p.innerHTML = "New Text";
+    p.className = "p";
+
+    innerHTML.bind({
+      onSwitch: noop,
+    })(doc.querySelector("p"), p);
+
+    doc.querySelector("p")!.innerHTML.should.equal("New Text");
+    doc.querySelector("p")!.className.should.equal("p");
+    doc.querySelector("p")!.id.should.equal("p");
+
+    p.removeAttribute("class");
+
+    innerHTML.bind({
+      onSwitch: noop,
+    })(doc.querySelector("p"), p);
+
+    doc.querySelector("p")!.className.should.equal("");
+  });
+});
diff --git a/test/switchesSelectors.ts b/test/switchesSelectors.ts
new file mode 100644
index 0000000..dc3309f
--- /dev/null
+++ b/test/switchesSelectors.ts
@@ -0,0 +1,74 @@
+import chai from "chai";
+import "./jsdom";
+import switchesSelectors from "../src/switchesSelectors";
+
+const should = chai.should();
+
+const noop = () => {};
+
+const pjax = {
+  onSwitch: () => {
+    console.log("Switched");
+  },
+  state: {},
+  log: noop,
+};
+
+describe("switchesSelectors", () => {
+  it("test switchesSelectors", () => {
+    const tmpEl = document.implementation.createHTMLDocument();
+
+    // a div container is used because swapping the containers
+    // will generate a new element, so things get weird
+    // using "body" generates a lot of testling cruft that I don't
+    // want so let's avoid that
+    const container = document.createElement("div");
+    container.innerHTML = "<p>Original Text</p><span>No Change</span>";
+    document.body.appendChild(container);
+
+    const container2 = tmpEl.createElement("div");
+    container2.innerHTML = "<p>New Text</p><span>New Span</span>";
+    tmpEl.body.appendChild(container2);
+
+    switchesSelectors.bind(pjax)(
+      {}, // switches
+      {}, // switchesOptions
+      ["p"], // selectors,
+      tmpEl, // fromEl
+      document, // toEl,
+      {} // options
+    );
+
+    container.innerHTML.should.equal("<p>New Text</p><span>No Change</span>");
+  });
+
+  it("test switchesSelectors when number of elements don't match", () => {
+    const newTempDoc = document.implementation.createHTMLDocument();
+    const originalTempDoc = document.implementation.createHTMLDocument();
+  
+    // a div container is used because swapping the containers
+    // will generate a new element, so things get weird
+    // using "body" generates a lot of testling cruft that I don't
+    // want so let's avoid that
+    const container = originalTempDoc.createElement("div");
+    container.innerHTML = "<p>Original text</p><span>No change</span>";
+    originalTempDoc.body.appendChild(container);
+  
+    const container2 = newTempDoc.createElement("div");
+    container2.innerHTML =
+      "<p>New text</p><p>More new text</p><span>New span</span>";
+    newTempDoc.body.appendChild(container2);
+  
+    const switchSelectorsFn = switchesSelectors.bind(
+      pjax,
+      {}, // switches
+      {}, // switchesOptions
+      ["p"], // selectors,
+      newTempDoc, // fromEl
+      originalTempDoc, // toEl,
+      {} // options
+    );
+    should.Throw(switchSelectorsFn);
+  
+  });
+});
diff --git a/test/util/contains.ts b/test/util/contains.ts
new file mode 100644
index 0000000..0919f85
--- /dev/null
+++ b/test/util/contains.ts
@@ -0,0 +1,20 @@
+import chai from "chai";
+import "../jsdom";
+import contains from "../../src/util/contains";
+
+const should = chai.should();
+
+describe("contains", () => {
+  it("test contains function", () => {
+    const tempDoc = document.implementation.createHTMLDocument();
+    tempDoc.body.innerHTML =
+      "<div><p id='el' class='js-Pjax'></p></div><span></span>";
+    let selectors = ["div"];
+    const el = tempDoc.body.querySelector("#el")!;
+    contains(tempDoc, selectors, el).should.equal(true);
+
+    selectors = ["span"];
+
+    contains(tempDoc, selectors, el).should.equal(false);
+  });
+});
diff --git a/test/util/updateQueryString.ts b/test/util/updateQueryString.ts
new file mode 100644
index 0000000..555758c
--- /dev/null
+++ b/test/util/updateQueryString.ts
@@ -0,0 +1,21 @@
+import chai from "chai";
+import "../jsdom";
+import updateQueryString from "../../src/util/updateQueryString";
+
+const should = chai.should();
+
+describe("updateQueryString", () => {
+  it("test update query string method", () => {
+    const url = "http://example.com";
+    let updatedUrl = updateQueryString(url, "foo", "bar");
+
+    url.should.not.equal(updatedUrl);
+    updatedUrl.should.equal(url + "?foo=bar");
+
+    updatedUrl = updateQueryString(updatedUrl, "foo", "baz");
+    updatedUrl.should.equal(url + "?foo=baz");
+
+    updatedUrl = updateQueryString(updatedUrl, "bar", "");
+    updatedUrl.should.equal(url + "?foo=baz&bar=");
+  });
+});
diff --git a/tsconfig.json b/tsconfig.json
index dc2d0d2..65a8960 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,8 @@
     "sourceMap": true,
     "declaration": true,
     "esModuleInterop": true,
-    "lib": ["dom", "ESNext"]
+    "lib": ["dom", "ESNext"],
+    "types": ["mocha"]
   },
   "include": ["./src/**/*"],
   "exclude": ["node_modules"]