Transcript
00:00 If I want to mock any API reliably, I need to first understand how my application uses it. So in the case of location suggestions, I can see that there is a React query with a query function that runs on the user input when they are typing into the location field and performs a GET request to this third party API, providing the value of the input field as the queue search parameter and expecting a JSON response back. And once it gets the locations from the response, it filters them to only include those with the address type of city or country. This information is important to me because it tells me what kind of request I need to intercept and what mock response to produce in order for my application to render the suggestions. And to do both of those things, I will use a library called mock service worker or msw.
00:45 I will open my terminal and run npm install msw and also add msw slash playwright which is the official binding to help integrating msw with playwright as dev dependencies. Good. And I will use msw as a custom fixture in playwright. So I will head to test extend.ts where I have all my custom fixtures and add a new one called network. So in my fixtures type definitions I will add the network key, but I won't annotate this fixture manually, instead I will use the existing type definitions from the MSW slash Playwright package.
01:17 So I will head at the top of the file and import from MSW slash Playwright the type network fixture and use this type over here as the call signature for my network fixture. Now the type story here is done and I need to provide the runtime implementation. So down there where I'm extending the test base, I will list my network fixture and implement it as a synchronous function. As always, we have test context and the use. And the first thing I will do is call the function called define network fixture from MSW slash playwright.
01:50 This function accepts an options object that needs the context. I will get this context from here, which is the test context I have from playwright, and the context itself is the BrowserContext object. And I am passing the BrowserContext here as an argument because the network interception will affect not just individual pages but the entire BrowserContext, which means if I create new pages as a part of my test case, they will still abide by my network description. That's exactly what I want. And now I can also pass the handlers argument which is the array of initial handlers that I have.
02:21 Basically functions affecting the network. The result of this function is an object that allows me to control the network interception. And I will call it network. Right after that, I will call network. Enable to enable the network interception and await this call because it returns a promise.
02:40 Then I will use the use function and provide the network object as the value of this fixture so I am able to use this object and its methods right from my test cases. And finally, I will clear up after myself by calling network. Disable as the name suggests to turn off the network interception when the test is done. Now, an important detail here is that similar to Vitest fixtures, fixtures in Playwright are lazy which means they only affect those test cases that explicitly import them. But that's not what I want for my network interception because I want it to apply to all test cases.
03:13 To do that, I will adjust this network fixture to be automatic. I will wrap the value of this fixture in an array where the first item will still be the fixture implementation, and I will provide the second item here as the fixture options, listing auto and setting it to true. So by doing this I'm telling playwrights to always run this fixture, so it's setup step, the usage, and then the disabling this fixture, the cleanup, no matter if the test case is referencing it or not. Now that the network fixture is ready, it's time to use it, so I will head to the create notes test case and get the network fixture. Now before doing anything else, I want to call network.
03:54 Use and provide this function with a list of request handlers that describe the network I want for this test. And because I want to intercept a request to this third party location service, I will import HTTP from MSW because this is an HTTP request and tap into HTTP. GET to target the GET request. This function accepts two arguments. The first one is a predicate to control which outgoing requests will match this handler, and the second one is the resolver which will handle this intercepted request.
04:23 As the predicate, I will provide the URL of this service. I will copy it from my hook and put it in the handler. And as the second argument, I'll provide a function, and here I will decide what to do with this request. I want to return a mock response here that contains a fixed list of location suggestions. So I will return an HTTP response from msw.json, which means a JSON response.
04:47 And as the response body, I will use an array of the following suggestions, each having place ID, address type of city, and display name of San Francisco and San Jose. Now at this point, this will return this mock response as is, but I am running at the risk of my mock getting out of sync from the actual API and the responses it returns. And one of the most straightforward ways to fix that is to make this mock type safe. And to do that, I will provide type arguments to this http.get call. It accepts three type arguments, the first one being the path parameters, and we don't have any in the URL, so I will just provide never.
05:22 The second type argument is the request body type, and because this is a get request, I will also provide never. And now the third one is the type that describes the response body. In here, I will use the type I already have for my third party API, which is this nominative search response type. Once I provided these type arguments, my mock becomes type safe. So if I make a mistake and provide the wrong value, TypeScript will let me know that hey, I should only provide country or city here, which is very handy.
05:50 Now that I've described the network I want, it's time to complete the test. I'm already at the create note page, which I can see here by this navigate call, so the next thing to do is to fill in the form that creates a new note. I will find the element by its label, get it by label, called title, and fill in something like my note and then another text area in this case by label content where I will provide the content of the note, maybe something like hello world. And now it's the star of the show providing the location for my note. I will start by finding an input by its label called location and just storing it in a variable called location input.
06:30 Next I will call location input dot fill and start typing the city in this case that I want to associate with my note, awaiting this. So since I am typing San, I am expecting my application to display both San Francisco and San Jose. So once I type this, let's assert that my application displays those suggestions. I will do so by locating an element by the role listbox and accessible name location suggestions. And then I will chain another locator here, another get by role when I want to target only option elements, which means I'm finding the list box with this accessible name and then targeting all the elements with the role option, which are the location suggestions.
07:13 And now I expect all of them to have text intent of San Francisco and San Jose. And now I want to select one of those locations. So what I will do, I will first store this list box in a variable called location suggestions and adjust this assertion to reference it and get all the option elements. And I will use a similar locator to select a specific suggestion by providing it an accessible name of that suggestion, for example San Francisco. Calling click and awaiting this action.
07:48 And then briefly making sure that the location input has value that I've just selected, which is San Francisco. So I'll copy it here and await it. I've provided all the information to create this note, so I will just click on the button with the name submit to submit it and create it. And now to make sure that the application displays the properly created note with the location that I chose. So So I'll have another expectation over element by role heading with the name my note, which is the name of my note, to be visible.
08:25 And then another element with the role note and accessible name location should have text with my note's location, which is San Francisco. And now let's run this test by opening the terminal and running npm run test and dwint. I can see this test failing and if I scroll up to the fail assertion I can see that it fails on the assertion for location suggestions to have the array of these cities, but it actually receives an empty array. This is happening because Playwright is interacting with our application way too fast in the test, faster than this client side logic for suggestions has any chance to trigger, and sometimes even faster than the hydration time of your app. That behavior is intentional so that Playwright could resolve your locators much faster without having to await for any external side effects such as the network.
09:26 But in some cases, such as ours, we have to make sure we await for the network before interacting with the network dependent elements. So to do that, I will head at the beginning of the test. And right after navigation, I will call page. WaitForLoadState and choose network idle. By doing so, I am telling the test before you continue, please make sure that there are no pending requests in the network, like the request for fetching JavaScript modules or interacting with third party APIs.
09:53 And if I run the test now I will see it passing. That is because Playwright is awaiting the idle network before continuing with the rest of the test, before creating the node and triggering the location suggestions. Let's have a quick recap of what we did here. So first, we introduced a new Playwright fixture called network using both MSW and atmsw slash playwright binding. We defined the fixture, provided initial handlers and the browser context, enabled the network interception, exposed that fixture for a test so we're able to control it, and, for example, assign additional request handlers, which is exactly what we did.
10:41 And then we disable the network when the test is done and this fixture is no longer needed. With a tiny catch of making this fixture automatic, which means that we don't have to reference it explicitly in every test case for it to affect them. And then in the test cases themselves, I use this fixture over here to provision this override by calling network dot use and providing this HTTP GET request handler. It intercepts our third party API request and provides a mock JSON response using this type arguments and the existing response type to make sure that these data structures that we provide as JSON remains type safe and aligned with what this API would actually return. And past that is just a regular test, filling in the inputs on the page, triggering the suggestion, and then asserting that all these suggestions are visible in the UI, completing the form, and asserting that the node indeed has the correct location displayed for the user.
11:37 There is one last thing I can recommend to make VMSW integration with Playwright even better. If you head back to test extend and take a look at our fixture, we can improve these initial handlers from being an empty array to becoming an option in Playwright. You can think of options as static fixtures that provide values to your tests, and you can control those values both on the individual test case level as well as setting them for the entire task project in your Playwright config. And the area of request handlers is a great candidate to become an option. So to do that, I will first declare the types for my options.
12:11 I will create a new interface and call it test options where I will have just one key called handlers with the type array of any handler, and this type comes from MSW. Now I will use this test options and merge it with the type of fixtures here where I'm extending the base test function. By doing so now, I will have handlers as a type safe option, and I will provide its value as an array. Well, the first item will be the default value for this option, so it's an empty array of handlers by default. And the second will be the options object where I will set option to true to let Playwright know that I intend to use this handler's key as an option and not as a fixture.
12:51 And now all that remains is to refactor the network fixture to grab the handlers from the context and provide it to the define network fixture function, so now it's coupled with playwright. Now what you can do with this is, for example, provide the handlers as the base layer for the entire test file by calling test dot use and providing the handlers array where you can describe whatever request handlers you want, and they will affect this entire test file. This can be really handy if you have a bunch of test cases that all rely on the same or similar network behaviors. But you can also provide the handlers option to entire task projects. And to do that, just make sure to export your interface, your test options.
13:32 And now you can head to your playwright config and provide this type to your define config function as the type arguments of cast options, which is mine from test extend. And after that, use them in your project. So here I have my project. I can go to the use object and specify handlers. And, hey, it's picked up.
13:49 And I can use it to provide a default list of request handlers that will affect all the tests in this project.
