使用 Puppeteer 捕获 HTTP 响应的正确方法是什么?

What is the proper way to capture a HTTP response with Puppeteer?

我正在尝试从用户注册中获取 HTTP 响应状态。

我的代码如下所示:

  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)
  })

文档给出了一个拦截请求并用它做事的例子:

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

我尝试过类似的模式,但没有成功,还有许多其他模式。我所做的一切 returns page,一个巨大的物体,上面没有我能看到的状态。任何帮助深表感谢。

有效的方法:

感谢@tomahaug 引导我朝着正确的方向前进。我的第一个问题是放置,需要在发出请求之前设置监听器,我在请求之后就设置好了。说得通。我最大的问题是将侦听器分配给一个变量,这样我就可以调用 expect 作为我的最后一行。将其分配给变量会导致返回 page。我需要做的只是 运行 监听器内部的测试。在为我使用 done() throws 和 error 时,我如下关闭了我的测试,我的代码的工作版本:

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()
  })

希望这对其他人有帮助!

我相信你应该按照这些思路做些事情。注意回调函数 done.

代码的作用是附加一个侦听器以获取响应,然后单击提交按钮。当收到响应时,它会检查状态代码,断言它,并通过调用 done 终止测试。

您可能希望有一个 if 语句来检查您在回调中检查的是否是来自表单的实际响应,因为响应处理程序可能会为其他并发请求发出事件。

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})
})

我没有测试代码,但它应该给你正确的想法。

编辑:进行了调整以反映最终的结果。

response.url 是一个函数,你必须调用它:

response.url()

response.request().method也一样:

response.request().method()

如果您需要操纵 request/response,请使用 page.setRequestInterception(true) and page.on/page.once (as documented)。

但是,如果您只需要对响应断言,最简单和最惯用的方法是 page.waitForResponse:

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

这允许测试流程保持线性并避免在 page.on 处理程序接收到 response 事件之前关闭测试的歧义。

已接受的答案(也已编辑到问题中)不正确。由于添加到点击调用的 1 秒延迟,它引入了竞争条件。充其量,这会不必要地减慢测试套件的速度,而在最坏的情况下,如果请求需要超过一秒的时间来解决,它会产生错误的失败(如果它被模拟,则不太可能,但它不会改变代码不安全的事实)。

只要 Jest 测试用例中有回调,确保它已被执行并且所有依赖于它的断言都已触发而不添加人为延迟的正确方法是 call done() from the callback。如果回调中有抛出导致 done 无法访问,则在错误处理程序中调用 done(error) 以向 Jest 报告测试用例失败。

为此,您需要将 done 作为参数添加到传递给 ittestonly 函数的回调中,以便它在块中可用。这允许 Jest 的测试运行器将测试视为异步测试,并且在调用 done 之前不解析它。如果没有 done,测试套件将忽略回调的断言。 async/await 没有帮助,因为它是一个独立于回调的异步链。

您只需将 done 指定为参数或 return 承诺(async 隐含地 return 是承诺),而不必同时指定两者。但是,您仍然可能希望对 Puppeteer 库调用使用 await 而不是 then。您可以使用 async IIFE,它最终会在所有断言都已触发时触发 done() 调用,以获得两全其美的效果。

例如,

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"]');
  })();
});

考虑到这一点, shows a likely better approach using waitForResponse 可以让您完全跳过回调和 donewaitForResponse 的回调是一个字符串 URL 或函数谓词,对于正在等待的目标响应应该 return 为真:

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"]');
  const response = await page.waitForResponse(response =>
    response.request().method === 'POST' && 
    response.url === `${process.env.USERS_API_DOMAIN}/sessions`
  );
  expect(response.status).toEqual(400);
});

我还应该提到 waitFor 在上面的代码片段中被弃用,取而代之的是 waitForSelector,并且 .url.method 是函数。我还没有验证上面的代码;它与原始 post 相关并显示高级模式。


最小示例

index.html

这是我们正在测试的网页。

<!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.jsasync/await版本):

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

index.test.jsthen版本):

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,
    };
    return page.goto("http://localhost:1234", {
        waitUntil: "load"
      })
      .then(() => page.click("button"))
      .then(() => page.waitForResponse(response =>
        response.request().method() === "POST" &&
        response.url() === url
      ))
      .then(response => response.json())
      .then(body => expect(body).toEqual(expectedBody))
    ;
  });
});

index.test.jsdone版本):

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"
      });
      page.click("button");
    })();
  });
});