Skip to content

Commit 47c5d2f

Browse files
committed
Automatically active script response
1 parent 11a0cd9 commit 47c5d2f

File tree

4 files changed

+64
-22
lines changed

4 files changed

+64
-22
lines changed

__tests__/fetch_request.js

+14
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

+30-22
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()

src/fetch_request.js

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export class FetchRequest {
2525
return Promise.reject(window.location.href = response.authenticationURL)
2626
}
2727

28+
if (response.isScript) {
29+
await response.activeScript()
30+
}
31+
2832
const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity
2933

3034
if (responseStatusIsTurboStreamable && response.isTurboStream) {
@@ -103,6 +107,8 @@ export class FetchRequest {
103107
return 'text/vnd.turbo-stream.html, text/html, application/xhtml+xml'
104108
case 'json':
105109
return 'application/json, application/vnd.api+json'
110+
case 'script':
111+
return 'text/javascript, application/javascript'
106112
default:
107113
return '*/*'
108114
}

src/fetch_response.js

+14
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(/\bjavascript\b/)
66+
}
67+
6468
async renderTurboStream () {
6569
if (this.isTurboStream) {
6670
if (window.Turbo) {
@@ -72,4 +76,14 @@ 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+
script.innerHTML = await this.text
84+
document.body.appendChild(script)
85+
} else {
86+
return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`))
87+
}
88+
}
7589
}

0 commit comments

Comments
 (0)