Skip to content

Commit

Permalink
Automatically active script response
Browse files Browse the repository at this point in the history
  • Loading branch information
songjiz committed Apr 8, 2024
1 parent 11a0cd9 commit 47c5d2f
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 22 deletions.
14 changes: 14 additions & 0 deletions __tests__/fetch_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
52 changes: 30 additions & 22 deletions __tests__/fetch_response.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,45 @@ 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("<h1>hi</h1>", { status: 200, headers: new Headers({'Content-Type': 'application/html'}) })
const testResponse = new FetchResponse(mockResponse)


expect(await testResponse.html).toBe("<h1>hi</h1>")
expect(await testResponse.html).toBe("<h1>hi</h1>")
expect(await testResponse.html).toBe("<h1>hi</h1>")
})
test('rejects on invalid content-type', async () => {
const mockResponse = new Response("<h1>hi</h1>", { status: 200, headers: new Headers({'Content-Type': 'text/plain'}) })
const testResponse = new FetchResponse(mockResponse)

expect(testResponse.html).rejects.toBeInstanceOf(Error)
})
})
describe('json', () => {
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)
})
test('rejects on invalid content-type', async () => {
const mockResponse = new Response("<h1>hi</h1>", { status: 200, headers: new Headers({'Content-Type': 'text/json'}) })
const testResponse = new FetchResponse(mockResponse)

expect(testResponse.json).rejects.toBeInstanceOf(Error)
})
})
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('body accessors', () => {
const warningSpy = jest.spyOn(console, 'warn').mockImplementation()

await testResponse.renderTurboStream()

expect(warningSpy).toBeCalled()
})
test('calls turbo', async () => {
Expand All @@ -99,10 +99,18 @@ describe('body accessors', () => {
test('rejects on invalid content-type', async () => {
const mockResponse = new Response("<h1>hi</h1>", { 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', () => {
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions src/fetch_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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) {
Expand Down Expand Up @@ -103,6 +107,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 '*/*'
}
Expand Down
14 changes: 14 additions & 0 deletions src/fetch_response.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export class FetchResponse {
return this.contentType.match(/^text\/vnd\.turbo-stream\.html/)
}

get isScript () {
return this.contentType.match(/\bjavascript\b/)
}

async renderTurboStream () {
if (this.isTurboStream) {
if (window.Turbo) {
Expand All @@ -72,4 +76,14 @@ 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')
script.innerHTML = await this.text
document.body.appendChild(script)
} else {
return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`))
}
}
}

0 comments on commit 47c5d2f

Please sign in to comment.