-
-
Notifications
You must be signed in to change notification settings - Fork 315
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Try to extract the logic to render the content of the dialog and the error handing into a separate component.
- Loading branch information
1 parent
e3fcc6b
commit fc9ba6c
Showing
6 changed files
with
249 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { translate } from "alchemy_admin/i18n" | ||
|
||
/** | ||
* the remote partial will automatically load the content of the given url and | ||
* put the fetched content into the inner component. It also handles different | ||
* kinds of error cases. | ||
*/ | ||
class RemotePartial extends HTMLElement { | ||
constructor() { | ||
super() | ||
this.addEventListener("ajax:success", this) | ||
this.addEventListener("ajax:error", this) | ||
} | ||
|
||
/** | ||
* handle the ajax event from inner forms | ||
* this is an intermediate solution until we moved away from jQuery | ||
* @param {CustomEvent} event | ||
* @deprecated | ||
*/ | ||
handleEvent(event) { | ||
/** @type {String} status */ | ||
const status = event.detail[1] | ||
/** @type {XMLHttpRequest} xhr */ | ||
const xhr = event.detail[2] | ||
|
||
switch (event.type) { | ||
case "ajax:success": | ||
const isTextResponse = xhr | ||
.getResponseHeader("Content-Type") | ||
.match(/html/) | ||
|
||
if (isTextResponse) { | ||
this.innerHTML = xhr.responseText | ||
} | ||
break | ||
case "ajax:error": | ||
this.#showErrorMessage(status, xhr.responseText, "error") | ||
break | ||
} | ||
} | ||
|
||
/** | ||
* show the spinner and load the content | ||
* after the content is loaded the spinner will be replaced by the fetched content | ||
*/ | ||
connectedCallback() { | ||
this.innerHTML = `<alchemy-spinner size="medium"></alchemy-spinner>` | ||
|
||
fetch(this.url, { | ||
headers: { "X-Requested-With": "XMLHttpRequest" } | ||
}) | ||
.then(async (response) => { | ||
if (response.ok) { | ||
if (response.redirected) { | ||
this.#showErrorMessage( | ||
translate("You are not authorized!"), | ||
translate("Please close this window.") | ||
) | ||
} else { | ||
this.innerHTML = await response.text() | ||
} | ||
} else { | ||
this.#showErrorMessage( | ||
response.statusText, | ||
await response.text(), | ||
"error" | ||
) | ||
} | ||
}) | ||
.catch(() => { | ||
this.#showErrorMessage( | ||
translate("The server does not respond."), | ||
translate("Please check server and try again.") | ||
) | ||
}) | ||
} | ||
|
||
/** | ||
* @param {string} title | ||
* @param {string} description | ||
* @param {"warning"|"error"} type | ||
*/ | ||
#showErrorMessage(title, description, type = "warning") { | ||
this.innerHTML = ` | ||
<alchemy-message type="${type}"> | ||
<h1>${title}</h1> | ||
<p>${description}</p> | ||
</alchemy-message> | ||
` | ||
} | ||
|
||
get url() { | ||
return this.getAttribute("url") | ||
} | ||
} | ||
|
||
customElements.define("alchemy-remote-partial", RemotePartial) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
spec/javascript/alchemy_admin/components/remote_partial.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import "alchemy_admin/components/remote_partial" | ||
|
||
describe("alchemy-remote-partial", () => { | ||
/** | ||
* @type {RemotePartial | undefined} | ||
*/ | ||
let partial = undefined | ||
|
||
const renderComponent = (url = null) => { | ||
document.body.innerHTML = `<alchemy-remote-partial url="${url}"></alchemy-remote-partial>` | ||
partial = document.querySelector("alchemy-remote-partial") | ||
return new Promise((resolve) => { | ||
setTimeout(() => resolve()) | ||
}) | ||
} | ||
|
||
/** | ||
* @param {{ | ||
* ok: boolean|undefined, | ||
* redirected: boolean|undefined, | ||
* statusText: string|undefined, | ||
* text: function|undefined | ||
* }} response | ||
*/ | ||
const mockFetch = (response = {}) => { | ||
global.fetch = jest.fn(() => | ||
Promise.resolve({ | ||
ok: true, | ||
redirected: false, | ||
statusText: "", | ||
text: () => Promise.resolve("Foo"), | ||
...response | ||
}) | ||
) | ||
} | ||
|
||
it("should render a spinner as initial content", () => { | ||
mockFetch() | ||
renderComponent() | ||
expect(document.querySelector("alchemy-spinner")).toBeTruthy() | ||
}) | ||
|
||
it("should fetch the given url", () => { | ||
mockFetch() | ||
renderComponent("http://foo.bar") | ||
expect(fetch).toHaveBeenCalledWith("http://foo.bar", { | ||
headers: { "X-Requested-With": "XMLHttpRequest" } | ||
}) | ||
}) | ||
|
||
describe("fetched url", () => { | ||
describe("successful response", () => { | ||
it("should replace the spinner with the fetched content", async () => { | ||
mockFetch() | ||
await renderComponent() | ||
expect(partial.innerHTML).toBe("Foo") | ||
}) | ||
}) | ||
|
||
describe("server errors", () => { | ||
it("doesn't respond", async () => { | ||
mockFetch() | ||
fetch.mockImplementationOnce(() => Promise.reject()) | ||
|
||
await renderComponent() | ||
expect(partial.innerHTML).toContain("The server does not respond") | ||
}) | ||
|
||
it("response with an error if the server response with an error", async () => { | ||
mockFetch({ ok: false, statusText: "Ruby Error" }) | ||
|
||
await renderComponent() | ||
expect(partial.innerHTML).toContain("Ruby Error") | ||
}) | ||
|
||
it("response with an redirect", async () => { | ||
mockFetch({ redirected: true }) | ||
|
||
await renderComponent() | ||
expect(partial.innerHTML).toContain("You are not authorized!") | ||
}) | ||
}) | ||
}) | ||
|
||
describe("jQuery Remote Call", () => { | ||
const dispatchCustomEvent = ( | ||
eventName = "ajax:success", | ||
responseText = "Bar", | ||
contentType = "text/html" | ||
) => { | ||
const event = new CustomEvent(eventName, { | ||
bubbles: true, | ||
detail: [ | ||
{}, | ||
eventName === "ajax:success" ? "OK" : "Error", | ||
{ | ||
responseText, | ||
getResponseHeader: jest.fn().mockReturnValue(contentType) | ||
} | ||
] | ||
}) | ||
partial.dispatchEvent(event) | ||
} | ||
|
||
beforeEach(async () => { | ||
mockFetch() | ||
await renderComponent() | ||
}) | ||
|
||
describe("ajax:success - event", () => { | ||
it("should replace the partial content with html response", () => { | ||
dispatchCustomEvent() | ||
expect(partial.innerHTML).toContain("Bar") | ||
}) | ||
|
||
it("should not replace the partial content with json response", () => { | ||
dispatchCustomEvent("ajax:success", "Bar", "application/json") | ||
expect(partial.innerHTML).toContain("Foo") | ||
}) | ||
}) | ||
|
||
describe("ajax:error - event", () => { | ||
it("should replace the partial content with html response", () => { | ||
dispatchCustomEvent("ajax:error", "Very Broken") | ||
expect(partial.innerHTML).toContain("Very Broken") | ||
}) | ||
}) | ||
}) | ||
}) |