Skip to content

Commit

Permalink
Extract ajax and events into own utils modules
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdeyen committed Apr 16, 2020
1 parent d72e641 commit 48a5c13
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 49 deletions.
3 changes: 2 additions & 1 deletion app/javascript/alchemy/admin/node_tree.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Sortable from "sortablejs"
import { on, ajax } from "./utils"
import ajax from "./utils/ajax"
import { on } from "./utils/events"

function displayNodeFolders() {
document.querySelectorAll("li.menu-item").forEach((el) => {
Expand Down
47 changes: 0 additions & 47 deletions app/javascript/alchemy/admin/utils.js

This file was deleted.

124 changes: 124 additions & 0 deletions app/javascript/alchemy/admin/utils/__tests__/ajax.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import xhrMock from "xhr-mock"
import ajax from "../ajax"

const token = "s3cr3t"

beforeEach(() => {
document.head.innerHTML = `<meta name="csrf-token" content="${token}">`
xhrMock.setup()
})

describe("ajax('get')", () => {
it("sends X-CSRF-TOKEN header", async () => {
xhrMock.get("/users", (req, res) => {
expect(req.header("X-CSRF-TOKEN")).toEqual(token)
return res.status(200).body('{"message":"Ok"}')
})
await ajax("get", "/users")
})

it("sends Content-Type header", async () => {
xhrMock.get("/users", (req, res) => {
expect(req.header("Content-Type")).toEqual(
"application/json; charset=utf-8"
)
return res.status(200).body('{"message":"Ok"}')
})
await ajax("get", "/users")
})

it("sends Accept header", async () => {
xhrMock.get("/users", (req, res) => {
expect(req.header("Accept")).toEqual("application/json")
return res.status(200).body('{"message":"Ok"}')
})
await ajax("get", "/users")
})

it("returns JSON", async () => {
xhrMock.get("/users", (_req, res) => {
return res.status(200).body('{"email":"mail@example.com"}')
})
await ajax("get", "/users").then((res) => {
expect(res.data).toEqual({ email: "mail@example.com" })
})
})

it("JSON parse errors get rejected", async () => {
xhrMock.get("/users", (_req, res) => {
return res.status(200).body('email => "mail@example.com"')
})
expect.assertions(1)
await ajax("get", "/users").catch((e) => {
expect(e.message).toMatch("Unexpected token")
})
})

it("network errors get rejected", async () => {
xhrMock.get("/users", () => {
return Promise.reject(new Error())
})
expect.assertions(1)
await ajax("get", "/users").catch((e) => {
expect(e.message).toEqual("An error occurred during the transaction")
})
})

it("server errors get rejected", async () => {
xhrMock.get("/users", (_req, res) => {
return res.status(401).body('{"error":"Unauthorized"}')
})
expect.assertions(1)
await ajax("get", "/users").catch((e) => {
expect(e.error).toEqual("Unauthorized")
})
})

it("server errors parsing errors get rejected", async () => {
xhrMock.get("/users", (_req, res) => {
return res.status(401).body("Unauthorized")
})
expect.assertions(1)
await ajax("get", "/users").catch((e) => {
expect(e.message).toMatch("Unexpected token")
})
})
})

describe("ajax('post')", () => {
it("sends X-CSRF-TOKEN header", async () => {
xhrMock.post("/users", (req, res) => {
expect(req.header("X-CSRF-TOKEN")).toEqual(token)
return res.status(200).body('{"message":"Ok"}')
})
await ajax("post", "/users")
})

it("sends Content-Type header", async () => {
xhrMock.post("/users", (req, res) => {
expect(req.header("Content-Type")).toEqual(
"application/json; charset=utf-8"
)
return res.status(200).body('{"message":"Ok"}')
})
await ajax("post", "/users")
})

it("sends Accept header", async () => {
xhrMock.post("/users", (req, res) => {
expect(req.header("Accept")).toEqual("application/json")
return res.status(200).body('{"message":"Ok"}')
})
await ajax("post", "/users")
})

it("sends JSON data", async () => {
xhrMock.post("/users", (req, res) => {
expect(req.body()).toEqual('{"email":"mail@example.com"}')
return res.status(200).body('{"message":"Ok"}')
})
await ajax("post", "/users", { email: "mail@example.com" })
})
})

afterEach(() => xhrMock.teardown())
38 changes: 38 additions & 0 deletions app/javascript/alchemy/admin/utils/__tests__/events.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { on } from "../events"

describe("on", () => {
const callback = jest.fn()

beforeEach(() => {
document.body.innerHTML = `
<ul class="list">
<li class="first item"><span>One</span></li>
<li class="second item">Two</li>
</ul>
`
})

it("adds event listener to base node", () => {
const baseNode = document.querySelector(".list")
const spy = jest.spyOn(baseNode, "addEventListener")
on("click", ".list", ".item", callback)
expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
spy.mockReset()
})

it("event triggered on matching child node calls callback", () => {
const childNode = document.querySelector(".first.item")
on("click", ".list", ".item", callback)
childNode.click()
expect(callback).toHaveBeenCalledWith(expect.any(MouseEvent))
})

it("event triggered on child of registered target still calls callback", () => {
const child = document.querySelector(".first.item span")
on("click", ".list", ".item", callback)
child.click()
expect(callback).toHaveBeenCalledWith(expect.any(MouseEvent))
})

afterEach(() => callback.mockReset())
})
48 changes: 48 additions & 0 deletions app/javascript/alchemy/admin/utils/ajax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
function buildPromise(xhr) {
return new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 400) {
try {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status
})
} catch (error) {
reject(error)
}
} else {
try {
reject(JSON.parse(xhr.responseText))
} catch (error) {
reject(error)
}
}
}
xhr.onerror = () => {
reject(new Error("An error occurred during the transaction"))
}
})
}

function getToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]')
return metaTag.attributes.content.textContent
}

export default function ajax(method, url, data) {
const xhr = new XMLHttpRequest()
const promise = buildPromise(xhr)

xhr.open(method, url)
xhr.setRequestHeader("Content-type", "application/json; charset=utf-8")
xhr.setRequestHeader("Accept", "application/json")
xhr.setRequestHeader("X-CSRF-Token", getToken())

if (data) {
xhr.send(JSON.stringify(data))
} else {
xhr.send()
}

return promise
}
16 changes: 16 additions & 0 deletions app/javascript/alchemy/admin/utils/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function on(eventName, baseSelector, targetSelector, callback) {
document.querySelectorAll(baseSelector).forEach((baseNode) => {
baseNode.addEventListener(eventName, (evt) => {
const targets = Array.from(baseNode.querySelectorAll(targetSelector))
let currentNode = evt.target

while (currentNode !== baseNode) {
if (targets.includes(currentNode)) {
callback.call(currentNode, evt)
return
}
currentNode = currentNode.parentElement
}
})
})
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"jest": "^25.2.7",
"prettier": "^2.0.2",
"webpack": "^4.42.1",
"webpack-dev-server": "^3.10.3"
"webpack-dev-server": "^3.10.3",
"xhr-mock": "^2.5.1"
},
"jest": {
"globals": {
Expand Down

0 comments on commit 48a5c13

Please sign in to comment.