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
.