18

I am trying to capture the http response status from a user sign-up.

My code looks like this:

  it.only('returns a 400 response if email is taken', async () => {
    await page.goto(`${process.env.DOMAIN}/sign-up`)
    await page.waitFor('input[id="Full Name"]')

    await page.type('input[id="Full Name"]', 'Luke Skywalker')
    await page.type('input[id="Email"]', 'LukeSkywalker@voyage.com')
    await page.type('input[id="Password"]', 'LukeSkywalker123', {delay: 100})
    await page.click('input[type="submit"]', {delay: 1000})

    const response = await page.on('response', response => response)

    console.log('request status', response.status)
    // expect(response).toEqual(400)
  })

The docs give an example of intercepting the request and doing things with it:

await page.setRequestInterception(true);
page.on('request', request => {
  request.respond({
    status: 404,
    contentType: 'text/plain',
    body: 'Not Found!'
  });
});

And I have tried a similar pattern to no avail, along with many other patterns. Everything I do returns the page, a huge object with no status on it that I can see. Any help is much appreciated.

WHAT WORKED:

thank you to @tomahaug for steering me in the correct direction. My first problem was placement, the listener needs to go be set up before the request is made, I had it just after the request. Makes sense. My biggest issue was assigning the listener to a variable, so that I could call the expect as my last line. Assigning it to a variable caused the page to be returned. What I needed to do was just run the test inside the listener. While using done() throws and error for me I closed off my test as follows below, the working version of my code:

it.only('returns a 400 response if email is taken', async () => {
    await page.goto(`${process.env.DOMAIN}/sign-up`)
    await page.waitFor('input[id="Full Name"]')

    await page.type('input[id="Full Name"]', 'Luke Skywalker')
    await page.type('input[id="Email"]', 'LukeSkywalker@voyage1.com')
    await page.type('input[id="Password"]', 'LukeSkywalker123', {delay: 100})

    await page.on('response', response => {
      if (response.request().method === 'POST' && response.url === `${process.env.USERS_API_DOMAIN}/sessions`) {
        expect(response.status).toEqual(400)
      }
    })

    await page.click('input[type="submit"]', {delay: 1000})
  })

  after(async function () {
    await browser.close()
  })

Hope this helps someone else!

ggorlen
  • 44,755
  • 7
  • 76
  • 106
HolyMoly
  • 2,020
  • 3
  • 22
  • 35
  • Instead of "WHAT WORKED", It's better to add a [self answer](https://stackoverflow.com/help/self-answer). Your solution still looks like it has a race condition. Since you're using a callback, I'd use `done` instead of `async`, then call `done()` in the callback after the `expect` line. – ggorlen Nov 24 '20 at 07:32

4 Answers4

9

I believe you should do something along those lines. Note the callback function done.

What the code does, is that it attaches a listener for responses, then clicks the submit button. When a response is received it checks the status code, asserts it, and terminates the test by calling done.

You might want to have an if-statement that checks that it is the actual response from your form that you are checking in the callback, as the response handler might emit events for other concurrent requests.

it.only('returns a 400 response if email is taken', () => {
  await page.goto(`${process.env.DOMAIN}/sign-up`)
  await page.waitFor('input[id="Full Name"]')

  await page.type('input[id="Full Name"]', 'Luke Skywalker')
  await page.type('input[id="Email"]', 'LukeSkywalker@voyage.com')
  await page.type('input[id="Password"]', 'LukeSkywalker123', {delay: 100})

  page.on('response', (response) => {
    if (
      response.request().method === 'POST' && 
      response.url === `${process.env.USERS_API_DOMAIN}/sessions`) 
    {
      expect(response.status).toEqual(400)
    }
  })

  await page.click('input[type="submit"]', {delay: 1000})
})

I have not tested the code, but it should give you the right idea.

Edit: Adjusted to reflect what worked out in the end.

Thanwa Ch.
  • 125
  • 9
tomahaug
  • 1,446
  • 10
  • 12
  • Ahhh, I need to set up the listener BEFORE I do the submit, makes sense! I tried this and it seems things are still a little off. The console.log inside the listener gives me nothing, and the test passes even when I make the `.toEqual(123)` and even when I remove the `if` (just in case). I have found console logs inside listeners have not worked for me with puppeteer, not sure why. When I create a variable, and assign the listener's return to it (returning `response.status`) I can see that again, the entire page is being returned as the status. Got anything? Thank you so much for your answer! – HolyMoly Nov 01 '17 at 14:10
  • I think the reason why the test just passes, is because it never reaches the inside of the handler. I have experienced jest to not respect the `done` callback when the test callback has been specified as `async`. The handler is still asynchronous even if you put `await` in front of it. It is the action of assigning the handler that returns a promise, not the result of the handler itself. So `var something = await page.on(...)` will not provide you with anything put the page instance reference. I'll try to modify my code. – tomahaug Nov 01 '17 at 14:23
  • I've modified the code, which should either make you see some log statements or see a timeout from the test runner. – tomahaug Nov 01 '17 at 14:26
  • 1
    got it! woot woot! I spent my whole day yesterday wrestling with this....the issue, was, as you said... I cannot assign it to a variable, else it returns the whole page. I had no idea! Note, the `done()` threw me big fat errors `(node:54604) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): ReferenceError: request is not defined` and I had to remove it. But at the end, I got it ;) Thank you SO much!!! – HolyMoly Nov 01 '17 at 14:43
  • 1
    also thank you for the explanations, that is super helpful in understanding what was happening – HolyMoly Nov 01 '17 at 14:47
  • This code has a race condition which forces you to create an artificial 1 second delay. `done` rather than `async` is the correct way to use callbacks that must run assertions. – ggorlen Nov 24 '20 at 07:35
8

If you need to manipulate the request/response, use page.setRequestInterception(true) and page.on/page.once (as documented).

However, if all you need is to assert something about the response, the simplest and most idiomatic way to do so is with page.waitForResponse:

const updateDashboardResponse = await page.waitForResponse(response =>
  response.url().includes('updateDashboard')
);
expect(updateDashboardResponse.status()).toBe(200);

This allows test flow to remain linear and avoids ambiguity around closing a test before a page.on handler receives a response event.

ericsoco
  • 24,913
  • 29
  • 97
  • 127
4

The accepted answer (which was also edited into the question) is incorrect. It introduces a race condition due to a 1 second delay added to the click call. At best, this slows down the test suite unnecessarily, and at worst it generates false failures should the request take longer than a second to resolve (unlikely if it's mocked, but it doesn't change the fact that the code is unsafe).

Whenever there's a callback in a Jest test case, the correct way to ensure it's been executed and all assertions depending on it firing have been made without adding artificial delays is to call done() from the callback. If there is a throw in the callback that makes done unreachable, call done(error) in the error handler to report the test case failure to Jest.

To do this, you'll need to add done as the parameter to the callback passed to the it, test or only function so that it's available in the block. This allows Jest's test runner to treat the test as asynchronous and not to resolve it until done is called. Without done, the test suite ignores the callback's assertions. async/await doesn't help because it's a separate asynchronous chain than the callback.

You only need to specify done as a parameter or return a promise (async implicitly returns a promise), never both. However, you'd still likely want to use await for Puppeteer library calls rather than then. You can use an async IIFE that eventually fires the done() call when all assertions have fired to get the best of both worlds.

For example,

it.only('returns a 400 response if email is taken', done => {
  (async () => {
    page.on('response', response => {
      if (response.request().method === 'POST' && 
          response.url === `${process.env.USERS_API_DOMAIN}/sessions`) {
        try { /* try-catch pattern shown for illustration */
          expect(response.status).toEqual(400);
          done();
        } 
        catch (err) {
          done(err);
        }
      }
    });    
    await page.goto(`${process.env.DOMAIN}/sign-up`);
    await page.waitFor('input[id="Full Name"]');
    await page.type('input[id="Full Name"]', 'Luke Skywalker');
    await page.type('input[id="Email"]', 'LukeSkywalker@voyage.com');
    await page.type('input[id="Password"]', 'LukeSkywalker123', {delay: 100});
    await page.click('input[type="submit"]');
  })();
});

With this in mind, this answer shows a likely better approach using waitForResponse which lets you skip the callback and done entirely. The callback to waitForResponse is a string URL or function predicate that should return true for the target response that's being waited on:

it.only('returns a 400 response if email is taken', async () => {
  await page.goto(`${process.env.DOMAIN}/sign-up`);
  await page.waitFor('input[id="Full Name"]');
  await page.type('input[id="Full Name"]', 'Luke Skywalker');
  await page.type('input[id="Email"]', 'LukeSkywalker@voyage.com');
  await page.type('input[id="Password"]', 'LukeSkywalker123', {delay: 100});
  const responseP = page.waitForResponse(response =>
    response.request().method === 'POST' && 
    response.url === `${process.env.USERS_API_DOMAIN}/sessions`
  );
  await page.click('input[type="submit"]');
  const response = await responseP;
  expect(response.status).toEqual(400);
});

I should also mention waitFor is deprecated in favor of waitForSelector in the above snippets and that .url and .method are functions. I haven't verified the above code; it's there to relate to the original post and show the high-level patterns.


Minimal example

index.html

This is the web page we're testing.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <button>Post</button>
    <script>
      document
        .querySelector("button")
        .addEventListener("click", e =>
          fetch("https://jsonplaceholder.typicode.com/posts", {
              method: "POST",
              body: JSON.stringify({
                title: "foo",
                body: "bar",
                userId: 1,
              }),
              headers: {
                "Content-type": "application/json; charset=UTF-8",
              },
            })
            .then(response => response.json())
            .then(json => console.log(json))
        );
    </script>
  </body>
</html>

index.test.js (async/await version):

describe("index page", () => {
  it("should respond to POST", async () => {
    const url = "https://jsonplaceholder.typicode.com/posts";
    await page.goto("http://localhost:1234", {waitUntil: "load"});
    const responseP = page.waitForResponse(response =>
      response.request().method() === "POST" &&
      response.url() === url
    );
    await page.click("button");
    const response = await responseP;
    const expectedBody = {
      body: "bar",
      id: 101,
      title: "foo",
      userId: 1,
    };
    expect(await response.json()).toEqual(expectedBody);
  });
});

index.test.js (then version):

describe("index page", () => {
  it("should respond to POST", () => {
    const url = "https://jsonplaceholder.typicode.com/posts";
    const expectedBody = {
      body: "bar",
      id: 101,
      title: "foo",
      userId: 1,
    };
    const navP = page.goto("http://localhost:1234", {
      waitUntil: "load",
    });
    const responseP = navP.then(() =>
      page.waitForResponse(
        response =>
          response.request().method() === "POST" &&
          response.url() === url
      )
    );
    return navP
      .then(() => page.click("button"))
      .then(() => responseP)
      .then(response => response.json())
      .then(body => expect(body).toEqual(expectedBody));
  });
});

index.test.js (done version):

describe("index page", () => {
  it("should respond to POST", done => {
    (async () => {
      const url = "https://jsonplaceholder.typicode.com/posts";
      const expectedBody = {
        body: "bar",
        id: 101,
        title: "foo",
        userId: 1,
      };
      await page.setRequestInterception(true);
      page.on("response", async response => {
        if (response.request().method() === "POST" &&
            response.url() === url) {
          try {
            const body = await response.json();
            expect(body).toEqual(expectedBody);
            done();
          }
          catch (err) {
            done(err);
          }
        }
      });
      await page.goto("http://localhost:1234", {
        waitUntil: "load"
      });
      await page.click("button");
    })();
  });
});

If you're reusing page between tests, remember to remove page.on listeners with page.off.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
1

response.url is a function and you have to call it:

response.url()

The same is for response.request().method:

response.request().method()
Zoe
  • 27,060
  • 21
  • 118
  • 148
Dobrea Petrisor
  • 471
  • 4
  • 5
  • 2
    This is correct. Perhaps Puppeteer's API has changed since the other answers were written, but they should be edited; `url` and `method` are "private" members now, prefixed with an underscore and exposed via getter methods. – ericsoco Sep 27 '19 at 22:07