diff --git a/__tests__/fetch_request.js b/__tests__/fetch_request.js
index 649b84c..482da13 100644
--- a/__tests__/fetch_request.js
+++ b/__tests__/fetch_request.js
@@ -71,6 +71,20 @@ describe('perform', () => {
expect(renderSpy).toHaveBeenCalledTimes(1)
jest.clearAllMocks();
})
+
+ test('script request automatically calls activeScript', async () => {
+ const mockResponse = new Response('', { status: 200, headers: { 'Content-Type': 'application/javascript' }})
+ window.fetch = jest.fn().mockResolvedValue(mockResponse)
+ jest.spyOn(FetchResponse.prototype, "ok", "get").mockReturnValue(true)
+ jest.spyOn(FetchResponse.prototype, "isScript", "get").mockReturnValue(true)
+ const renderSpy = jest.spyOn(FetchResponse.prototype, "activeScript").mockImplementation()
+
+ const testRequest = new FetchRequest("get", "localhost")
+ await testRequest.perform()
+
+ expect(renderSpy).toHaveBeenCalledTimes(1)
+ jest.clearAllMocks();
+ })
})
test('treat method name case-insensitive', async () => {
diff --git a/__tests__/fetch_response.js b/__tests__/fetch_response.js
index f40de21..b73a65e 100644
--- a/__tests__/fetch_response.js
+++ b/__tests__/fetch_response.js
@@ -16,29 +16,29 @@ describe('body accessors', () => {
test('works multiple times', async () => {
const mockResponse = new Response("Mock", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
const testResponse = new FetchResponse(mockResponse)
-
+
+ expect(await testResponse.text).toBe("Mock")
expect(await testResponse.text).toBe("Mock")
- expect(await testResponse.text).toBe("Mock")
})
test('work regardless of content-type', async () => {
const mockResponse = new Response("Mock", { status: 200, headers: new Headers({'Content-Type': 'not/text'}) })
const testResponse = new FetchResponse(mockResponse)
-
- expect(await testResponse.text).toBe("Mock")
+
+ expect(await testResponse.text).toBe("Mock")
})
})
describe('html', () => {
test('works multiple times', async () => {
const mockResponse = new Response("
hi
", { status: 200, headers: new Headers({'Content-Type': 'application/html'}) })
const testResponse = new FetchResponse(mockResponse)
-
+
+ expect(await testResponse.html).toBe("hi
")
expect(await testResponse.html).toBe("hi
")
- expect(await testResponse.html).toBe("hi
")
})
test('rejects on invalid content-type', async () => {
const mockResponse = new Response("hi
", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
const testResponse = new FetchResponse(mockResponse)
-
+
expect(testResponse.html).rejects.toBeInstanceOf(Error)
})
})
@@ -46,7 +46,7 @@ describe('body accessors', () => {
test('works multiple times', async () => {
const mockResponse = new Response(JSON.stringify({ json: 'body' }), { status: 200, headers: new Headers({'Content-Type': 'application/json'}) })
const testResponse = new FetchResponse(mockResponse)
-
+
// works mutliple times
expect({ json: 'body' }).toStrictEqual(await testResponse.json)
expect({ json: 'body' }).toStrictEqual(await testResponse.json)
@@ -54,7 +54,7 @@ describe('body accessors', () => {
test('rejects on invalid content-type', async () => {
const mockResponse = new Response("hi
", { status: 200, headers: new Headers({'Content-Type': 'text/json'}) })
const testResponse = new FetchResponse(mockResponse)
-
+
expect(testResponse.json).rejects.toBeInstanceOf(Error)
})
})
@@ -85,7 +85,7 @@ describe('body accessors', () => {
const warningSpy = jest.spyOn(console, 'warn').mockImplementation()
await testResponse.renderTurboStream()
-
+
expect(warningSpy).toBeCalled()
})
test('calls turbo', async () => {
@@ -99,10 +99,18 @@ describe('body accessors', () => {
test('rejects on invalid content-type', async () => {
const mockResponse = new Response("hi
", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
const testResponse = new FetchResponse(mockResponse)
-
+
expect(testResponse.renderTurboStream()).rejects.toBeInstanceOf(Error)
})
})
+ describe('script', () => {
+ test('rejects on invalid content-type', async () => {
+ const mockResponse = new Response("", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
+ const testResponse = new FetchResponse(mockResponse)
+
+ expect(testResponse.activeScript()).rejects.toBeInstanceOf(Error)
+ })
+ })
})
describe('fetch response helpers', () => {
@@ -135,46 +143,46 @@ describe('fetch response helpers', () => {
})
})
describe('http-status helpers', () => {
-
+
test('200', () => {
const mockResponse = new Response(null, { status: 200 })
const testResponse = new FetchResponse(mockResponse)
-
+
expect(testResponse.statusCode).toBe(200)
expect(testResponse.ok).toBeTruthy()
- expect(testResponse.redirected).toBeFalsy()
+ expect(testResponse.redirected).toBeFalsy()
expect(testResponse.unauthenticated).toBeFalsy()
expect(testResponse.unprocessableEntity).toBeFalsy()
})
-
+
test('401', () => {
const mockResponse = new Response(null, { status: 401 })
const testResponse = new FetchResponse(mockResponse)
-
+
expect(testResponse.statusCode).toBe(401)
expect(testResponse.ok).toBeFalsy()
- expect(testResponse.redirected).toBeFalsy()
+ expect(testResponse.redirected).toBeFalsy()
expect(testResponse.unauthenticated).toBeTruthy()
expect(testResponse.unprocessableEntity).toBeFalsy()
})
-
+
test('422', () => {
const mockResponse = new Response(null, { status: 422 })
const testResponse = new FetchResponse(mockResponse)
-
+
expect(testResponse.statusCode).toBe(422)
expect(testResponse.ok).toBeFalsy()
- expect(testResponse.redirected).toBeFalsy()
+ expect(testResponse.redirected).toBeFalsy()
expect(testResponse.unauthenticated).toBeFalsy()
expect(testResponse.unprocessableEntity).toBeTruthy()
})
-
+
test('302', () => {
const mockHeaders = new Headers({'Location': 'https://localhost/login'})
const mockResponse = new Response(null, { status: 302, url: 'https://localhost/login', headers: mockHeaders })
jest.spyOn(mockResponse, 'redirected', 'get').mockReturnValue(true)
const testResponse = new FetchResponse(mockResponse)
-
+
expect(testResponse.statusCode).toBe(302)
expect(testResponse.ok).toBeFalsy()
expect(testResponse.redirected).toBeTruthy()
diff --git a/__tests__/request_interceptor.js b/__tests__/request_interceptor.js
index 57b3b2d..a708b31 100644
--- a/__tests__/request_interceptor.js
+++ b/__tests__/request_interceptor.js
@@ -1,11 +1,12 @@
/**
* @jest-environment jsdom
*/
+import 'isomorphic-fetch'
import { RequestInterceptor } from '../src/request_interceptor'
import { FetchRequest } from '../src/fetch_request'
beforeEach(() => {
- window.fetch = jest.fn().mockResolvedValue({ status: 200, body: "done" })
+ window.fetch = jest.fn().mockResolvedValue(new Response("success!", { status: 200, body: "done" }))
})
test('request intercepter is executed', async () => {
diff --git a/src/fetch_request.js b/src/fetch_request.js
index a5f3038..2d27f01 100644
--- a/src/fetch_request.js
+++ b/src/fetch_request.js
@@ -29,6 +29,10 @@ export class FetchRequest {
return Promise.reject(window.location.href = response.authenticationURL)
}
+ if (response.isScript) {
+ await response.activeScript()
+ }
+
const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity
if (responseStatusIsTurboStreamable && response.isTurboStream) {
@@ -107,6 +111,8 @@ export class FetchRequest {
return 'text/vnd.turbo-stream.html, text/html, application/xhtml+xml'
case 'json':
return 'application/json, application/vnd.api+json'
+ case 'script':
+ return 'text/javascript, application/javascript'
default:
return '*/*'
}
diff --git a/src/fetch_response.js b/src/fetch_response.js
index 75d7725..8f853a0 100644
--- a/src/fetch_response.js
+++ b/src/fetch_response.js
@@ -61,6 +61,10 @@ export class FetchResponse {
return this.contentType.match(/^text\/vnd\.turbo-stream\.html/)
}
+ get isScript () {
+ return this.contentType.match(/\b(?:java|ecma)script\b/)
+ }
+
async renderTurboStream () {
if (this.isTurboStream) {
if (window.Turbo) {
@@ -72,4 +76,17 @@ export class FetchResponse {
return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`))
}
}
+
+ async activeScript () {
+ if (this.isScript) {
+ const script = document.createElement('script')
+ const metaTag = document.querySelector('meta[name=csp-nonce]')
+ const nonce = metaTag && metaTag.content
+ if (nonce) { script.setAttribute('nonce', nonce) }
+ script.innerHTML = await this.text
+ document.body.appendChild(script)
+ } else {
+ return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`))
+ }
+ }
}