Skip to content

Commit 25324b0

Browse files
authored
Merge pull request #72 from songjiz/handle-script-response
Automatically active script response
2 parents 421fdc5 + d1188c8 commit 25324b0

File tree

5 files changed

+69
-23
lines changed

5 files changed

+69
-23
lines changed

__tests__/fetch_request.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ describe('perform', () => {
7171
expect(renderSpy).toHaveBeenCalledTimes(1)
7272
jest.clearAllMocks();
7373
})
74+
75+
test('script request automatically calls activeScript', async () => {
76+
const mockResponse = new Response('', { status: 200, headers: { 'Content-Type': 'application/javascript' }})
77+
window.fetch = jest.fn().mockResolvedValue(mockResponse)
78+
jest.spyOn(FetchResponse.prototype, "ok", "get").mockReturnValue(true)
79+
jest.spyOn(FetchResponse.prototype, "isScript", "get").mockReturnValue(true)
80+
const renderSpy = jest.spyOn(FetchResponse.prototype, "activeScript").mockImplementation()
81+
82+
const testRequest = new FetchRequest("get", "localhost")
83+
await testRequest.perform()
84+
85+
expect(renderSpy).toHaveBeenCalledTimes(1)
86+
jest.clearAllMocks();
87+
})
7488
})
7589

7690
test('treat method name case-insensitive', async () => {

__tests__/fetch_response.js

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,45 @@ describe('body accessors', () => {
1616
test('works multiple times', async () => {
1717
const mockResponse = new Response("Mock", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
1818
const testResponse = new FetchResponse(mockResponse)
19-
19+
20+
expect(await testResponse.text).toBe("Mock")
2021
expect(await testResponse.text).toBe("Mock")
21-
expect(await testResponse.text).toBe("Mock")
2222
})
2323
test('work regardless of content-type', async () => {
2424
const mockResponse = new Response("Mock", { status: 200, headers: new Headers({'Content-Type': 'not/text'}) })
2525
const testResponse = new FetchResponse(mockResponse)
26-
27-
expect(await testResponse.text).toBe("Mock")
26+
27+
expect(await testResponse.text).toBe("Mock")
2828
})
2929
})
3030
describe('html', () => {
3131
test('works multiple times', async () => {
3232
const mockResponse = new Response("<h1>hi</h1>", { status: 200, headers: new Headers({'Content-Type': 'application/html'}) })
3333
const testResponse = new FetchResponse(mockResponse)
34-
34+
35+
expect(await testResponse.html).toBe("<h1>hi</h1>")
3536
expect(await testResponse.html).toBe("<h1>hi</h1>")
36-
expect(await testResponse.html).toBe("<h1>hi</h1>")
3737
})
3838
test('rejects on invalid content-type', async () => {
3939
const mockResponse = new Response("<h1>hi</h1>", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
4040
const testResponse = new FetchResponse(mockResponse)
41-
41+
4242
expect(testResponse.html).rejects.toBeInstanceOf(Error)
4343
})
4444
})
4545
describe('json', () => {
4646
test('works multiple times', async () => {
4747
const mockResponse = new Response(JSON.stringify({ json: 'body' }), { status: 200, headers: new Headers({'Content-Type': 'application/json'}) })
4848
const testResponse = new FetchResponse(mockResponse)
49-
49+
5050
// works mutliple times
5151
expect({ json: 'body' }).toStrictEqual(await testResponse.json)
5252
expect({ json: 'body' }).toStrictEqual(await testResponse.json)
5353
})
5454
test('rejects on invalid content-type', async () => {
5555
const mockResponse = new Response("<h1>hi</h1>", { status: 200, headers: new Headers({'Content-Type': 'text/json'}) })
5656
const testResponse = new FetchResponse(mockResponse)
57-
57+
5858
expect(testResponse.json).rejects.toBeInstanceOf(Error)
5959
})
6060
})
@@ -85,7 +85,7 @@ describe('body accessors', () => {
8585
const warningSpy = jest.spyOn(console, 'warn').mockImplementation()
8686

8787
await testResponse.renderTurboStream()
88-
88+
8989
expect(warningSpy).toBeCalled()
9090
})
9191
test('calls turbo', async () => {
@@ -99,10 +99,18 @@ describe('body accessors', () => {
9999
test('rejects on invalid content-type', async () => {
100100
const mockResponse = new Response("<h1>hi</h1>", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
101101
const testResponse = new FetchResponse(mockResponse)
102-
102+
103103
expect(testResponse.renderTurboStream()).rejects.toBeInstanceOf(Error)
104104
})
105105
})
106+
describe('script', () => {
107+
test('rejects on invalid content-type', async () => {
108+
const mockResponse = new Response("", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
109+
const testResponse = new FetchResponse(mockResponse)
110+
111+
expect(testResponse.activeScript()).rejects.toBeInstanceOf(Error)
112+
})
113+
})
106114
})
107115

108116
describe('fetch response helpers', () => {
@@ -135,46 +143,46 @@ describe('fetch response helpers', () => {
135143
})
136144
})
137145
describe('http-status helpers', () => {
138-
146+
139147
test('200', () => {
140148
const mockResponse = new Response(null, { status: 200 })
141149
const testResponse = new FetchResponse(mockResponse)
142-
150+
143151
expect(testResponse.statusCode).toBe(200)
144152
expect(testResponse.ok).toBeTruthy()
145-
expect(testResponse.redirected).toBeFalsy()
153+
expect(testResponse.redirected).toBeFalsy()
146154
expect(testResponse.unauthenticated).toBeFalsy()
147155
expect(testResponse.unprocessableEntity).toBeFalsy()
148156
})
149-
157+
150158
test('401', () => {
151159
const mockResponse = new Response(null, { status: 401 })
152160
const testResponse = new FetchResponse(mockResponse)
153-
161+
154162
expect(testResponse.statusCode).toBe(401)
155163
expect(testResponse.ok).toBeFalsy()
156-
expect(testResponse.redirected).toBeFalsy()
164+
expect(testResponse.redirected).toBeFalsy()
157165
expect(testResponse.unauthenticated).toBeTruthy()
158166
expect(testResponse.unprocessableEntity).toBeFalsy()
159167
})
160-
168+
161169
test('422', () => {
162170
const mockResponse = new Response(null, { status: 422 })
163171
const testResponse = new FetchResponse(mockResponse)
164-
172+
165173
expect(testResponse.statusCode).toBe(422)
166174
expect(testResponse.ok).toBeFalsy()
167-
expect(testResponse.redirected).toBeFalsy()
175+
expect(testResponse.redirected).toBeFalsy()
168176
expect(testResponse.unauthenticated).toBeFalsy()
169177
expect(testResponse.unprocessableEntity).toBeTruthy()
170178
})
171-
179+
172180
test('302', () => {
173181
const mockHeaders = new Headers({'Location': 'https://localhost/login'})
174182
const mockResponse = new Response(null, { status: 302, url: 'https://localhost/login', headers: mockHeaders })
175183
jest.spyOn(mockResponse, 'redirected', 'get').mockReturnValue(true)
176184
const testResponse = new FetchResponse(mockResponse)
177-
185+
178186
expect(testResponse.statusCode).toBe(302)
179187
expect(testResponse.ok).toBeFalsy()
180188
expect(testResponse.redirected).toBeTruthy()

__tests__/request_interceptor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/**
22
* @jest-environment jsdom
33
*/
4+
import 'isomorphic-fetch'
45
import { RequestInterceptor } from '../src/request_interceptor'
56
import { FetchRequest } from '../src/fetch_request'
67

78
beforeEach(() => {
8-
window.fetch = jest.fn().mockResolvedValue({ status: 200, body: "done" })
9+
window.fetch = jest.fn().mockResolvedValue(new Response("success!", { status: 200, body: "done" }))
910
})
1011

1112
test('request intercepter is executed', async () => {

src/fetch_request.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export class FetchRequest {
2929
return Promise.reject(window.location.href = response.authenticationURL)
3030
}
3131

32+
if (response.isScript) {
33+
await response.activeScript()
34+
}
35+
3236
const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity
3337

3438
if (responseStatusIsTurboStreamable && response.isTurboStream) {
@@ -107,6 +111,8 @@ export class FetchRequest {
107111
return 'text/vnd.turbo-stream.html, text/html, application/xhtml+xml'
108112
case 'json':
109113
return 'application/json, application/vnd.api+json'
114+
case 'script':
115+
return 'text/javascript, application/javascript'
110116
default:
111117
return '*/*'
112118
}

src/fetch_response.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export class FetchResponse {
6161
return this.contentType.match(/^text\/vnd\.turbo-stream\.html/)
6262
}
6363

64+
get isScript () {
65+
return this.contentType.match(/\b(?:java|ecma)script\b/)
66+
}
67+
6468
async renderTurboStream () {
6569
if (this.isTurboStream) {
6670
if (window.Turbo) {
@@ -72,4 +76,17 @@ export class FetchResponse {
7276
return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`))
7377
}
7478
}
79+
80+
async activeScript () {
81+
if (this.isScript) {
82+
const script = document.createElement('script')
83+
const metaTag = document.querySelector('meta[name=csp-nonce]')
84+
const nonce = metaTag && metaTag.content
85+
if (nonce) { script.setAttribute('nonce', nonce) }
86+
script.innerHTML = await this.text
87+
document.body.appendChild(script)
88+
} else {
89+
return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`))
90+
}
91+
}
7592
}

0 commit comments

Comments
 (0)