mswjs / msw

Industry standard API mocking for JavaScript.
https://mswjs.io
MIT License
15.93k stars 517 forks source link

Make it possible to mock request for static files and images #457

Closed DamianPereira closed 3 years ago

DamianPereira commented 3 years ago

Right now I'm trying to test a component in storybook, and I'm using msw to mock the neccesary routes for this component to render. One thing this component needs is a user profile image, this user is generated at random in the tests, including the id of the profile image. This url changes everytime storybook runs since the users are auto-generated for the tests, so the url of the profile image changes too.

Ideally I want to do something like this in the handlers:

  rest.get('/api/files/:id', async (req, res, ctx) => {
    const image = await ctx.fetch('/user.jpg'); // static file served by storybook, or any image from a url
    return res(ctx.jpg(image));
  }),

Or even importing the image like storybook allows https://storybook.js.org/docs/react/configure/images-and-assets

  import userImage from '../assets/user.jpg'
  rest.get('/api/files/:id', async (req, res, ctx) => {
    return res(ctx.jpg(userImage));
  }),

I have tried using the ctx.body method to handle this, but it does not work:

  rest.get('/api/files/:id/content', async (req, res, ctx) => {
    const image = await ctx.fetch('/_user1.jpg');
    return res(ctx.body(image));
  }),

The mocked request generates a 500 error with this message: DOMException: Failed to execute 'postMessage' on 'MessagePort': Response object could not be cloned. (see more detailed error stack trace in the mocked response body)

kettanaito commented 3 years ago

Hey, @DamianPereira. Thanks for reaching out with this.

You can respond with mocked images/videos/audio and other media types with MSW. To do that, one needs to compose a proper binary response, as aforementioned media types are not strictly textual responses.

Composing such mocked response comes down to these crucial points:

  1. Specifying the right Content-Type and Content-Length response headers.
  2. Providing binary response data (i.e. a buffer) to res.body.

For example, to respond with a mocked image use the following handler:

import base64Image from 'url-loader!../fixtures/image.jpg'

rest.get('/api/files/:id/content', async (_, res, ctx) => {
  // Convert "base64" image to "ArrayBuffer".
  const imageBuffer = await fetch(base64Image).then((res) =>
    res.arrayBuffer(),
  )
  return res(
    ctx.set('Content-Length', imageBuffer.byteLength.toString()),
    ctx.set('Content-Type', 'image/jpeg'),
    // Respond with the "ArrayBuffer".
    ctx.body(imageBuffer),
  )
})

Learn more in the Binary response types in the documentation.

DamianPereira commented 3 years ago

Great thanks! Sorry I missed it in the docs. It still might be nice to have something like ctx.jpg that abstracts this further, but this will do for now.

kettanaito commented 3 years ago

That's the beauty of a functional API the team works hard on designing: instead of handling each individual use case, or bloating things with options and configurations, you are given a function primitives to abstract, reuse, and create.

Here's how you'd implement a custom response transformer to make your mock definitions more concise:

// mocks/ctx/jpeg.js
import { context } from 'msw'

export const jpeg = async (base64Image) => {
  const imageBuffer = await fetch(base64Image)
    .then(res => res.arrayBuffer())

  return (res) => {
    context.set('Content-Length', imageBuffer.byteLength.toStrong())(res)
    context.set('Content-Type', 'image/jpeg')(res)
    context.body(imageBuffer)(res)
  }
}
// mocks/handlers.js
import base64Image from 'url-loader!../fixtures/image.jpg'
import { jpeg } from './ctx/jpeg'

rest.get('...', async (req, res, ctx) => {
  // Use your custom response transformer
  const respondWithImage = await jpeg(base64Image)

  return res(respondWithImage)
})

Currently MSW doesn't support asynchronous response transformers, that's why the jpeg transformer needs to be awaited first, before providing it to the res() function.

DamianPereira commented 3 years ago

Oh, I like that, yeah that solves the problem nicely when having to serve multiple images, thanks!

mordvic commented 1 year ago

I have the problem: [MSW] Warning: captured a request without a matching request handler:

  • GET http://localhost/iVBORw0KGgoAAAANS....

export const setupFetchProviderConversationBlobHandler = () => { server.use( rest.get(/(.*)attachments\/\d+$/, async (_, res, ctx) => { const imageBuffer = await fetch(base64Image).then((res) => res.arrayBuffer()); return res( ctx.status(200), ctx.set('Content-Length', imageBuffer.byteLength.toString()), ctx.set('Content-Type', 'image/png'), ctx.body(imageBuffer) ); }) ); };

kettanaito commented 1 year ago

@mordvic, as the name suggests, the actual request URL didn't match the route handler you've defined for MSW. Take a closer look at the structure of your URL and reflect it in the request handler. I'm rather confident in our path matching (since we depend on third-party) so the error is likely on your end.