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/README.md b/README.md index 102cc04..399de1a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # theme-shokax-pjax -![NPM](https://img.shields.io/npm/l/theme-shokax-pjax) ![npm](https://img.shields.io/npm/v/theme-shokax-pjax) ![npm](https://img.shields.io/npm/dm/theme-shokax-pjax) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/theme-shokax-pjax) +![NPM](https://img.shields.io/npm/l/theme-shokax-pjax) ![npm](https://img.shields.io/npm/v/theme-shokax-pjax) ![npm](https://img.shields.io/npm/dm/theme-shokax-pjax) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/theme-shokax-pjax) [![Coverage Status](https://coveralls.io/repos/github/theme-shoka-x/theme-shokax-pjax/badge.svg?branch=main)](https://coveralls.io/github/theme-shoka-x/theme-shokax-pjax?branch=main) pjax for [hexo-theme-shokaX](https://github.com/theme-shoka-x/hexo-theme-shokaX) diff --git a/package.json b/package.json index 9a76e46..9b0c3ca 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=lcov --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 + | 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 = {} ): 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 - ) => 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 = ` + + `; + 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 = "

Original Text

"; + 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 = "

Original Text

"; + 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 = "

Original Text

No Change"; + document.body.appendChild(container); + + const container2 = tmpEl.createElement("div"); + container2.innerHTML = "

New Text

New Span"; + tmpEl.body.appendChild(container2); + + switchesSelectors.bind(pjax)( + {}, // switches + {}, // switchesOptions + ["p"], // selectors, + tmpEl, // fromEl + document, // toEl, + {} // options + ); + + container.innerHTML.should.equal("

New Text

No Change"); + }); + + 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 = "

Original text

No change"; + originalTempDoc.body.appendChild(container); + + const container2 = newTempDoc.createElement("div"); + container2.innerHTML = + "

New text

More new text

New 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 = + "

"; + 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"]