August 26th
This morning I am going through chapter 18 of Eloquent Javascript. (almost done with this book!) The chapter focuses on HTTP requests, html forms, as well as javascript’s fetch function.
I’ve worked with fetch in a couple of my own projects, to make calls to me own API on routes being served up by Express. Let’s dive in.
Fetch
Calling fetch
returns a promise that resolves to a response object containing information about the server’s response.
fetch("example/data.txt").then(response => {
console.log(response.status);
// → 200
console.log(response.headers.get("Content-Type"));
// → text/plain
});
NOTE: The promise returned by fetch
resolves even if the server responded with an error code. It may be rejected if there is a network error, or the address can’t be found.
To get the actual content of a response you can use its text
method, or a similar method called json
(which is what I’ve always used). The json
method though only resolves if it can parse as valid JSON.
fetch("example/data.txt")
.then(resp => resp.text())
.then(text => console.log(text));
// → This is the content of data.txt
fetch("example/data.txt")
.then(resp => resp.json())
.then(data => console.log(data));
{ // data shown here }
By default, fetch
uses the GET
method, but you can pass an object with extra options as a second argument to use others.
fetch("example/data.txt", {method: "DELETE"}).then(resp => {
console.log(resp.status);
// → 405
});
Other than this, this chapter is pretty straight forward when it comes to html forms. I haven’t seen anything I didnt already know.
Except…
File Fields
If a user selects a file for a file input field, the browser interprets that action to mean that the script may read the file. This will log to the console the name of the file chosen, as well as the file type if one is present.
<input type="file">
<script>
let input = document.querySelector("input");
input.addEventListener("change", () => {
if (input.files.length > 0) {
let file = input.files[0];
console.log("You chose", file.name);
if (file.type) console.log("It has type", file.type);
}
});
</script>
The file form field though does not have a property that contains the content of the file. Getting that is a little more involved, and must be asynchronous to avoid the document freezing up.
<input type="file" multiple>
<script>
let input = document.querySelector("input");
input.addEventListener("change", () => {
for (let file of Array.from(input.files)) {
let reader = new FileReader();
reader.addEventListener("load", () => {
console.log("File", file.name, "starts with",
reader.result.slice(0, 20));
});
reader.readAsText(file);
}
});
</script>
Reading a file is done by creating a FileReader
object, calling a “load” event handler on it, and then calling its readAsText
method. Once finished loading, the reader’s result
property contains the file’s content.
File readers also throw an error event when reading the file fails for any reason. The error will end up in the objects error property. Since this interface was designed before promises came into the picture, we can wrap these in a promise ourselves.
function readFileText(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.addEventListener(
"load", () => resolve(reader.result));
reader.addEventListener(
"error", () => reject(reader.error));
reader.readAsText(file);
});
}
Storing data client-side
In this section the book covers the localStorage
object available to us in the browser to store information outside of javascript variables in order to survive page reloads.
localStorage.setItem("username", "marijn");
console.log(localStorage.getItem("username"));
// → marijn
localStorage.removeItem("username");
Values in localStorage stick around until overwritten, removed with removeItem
or the user clears their local data.
Another important thing is that sites from different domains get different storage compartments. In principle, info stored to the localStorage
for a certain website, can only be read by the scripts on that website.
This chapter was pretty straightforward and was a nice summary of thing’s I’ve learned throughout my dev journey… Now onto the Usemy Node Course.
Mocking NPM Modules during testing
When running in a test environment, we may be running code that doesnt need to run in the background. For instance, if we are sending emails with sendGrid, our tests would continuously be sending out emails.
To start, we will create a mocks folder in our test directory. Jest will look here for any modules and use them as mocks if they are present. If I wanted to mock jsonwebtoken, I would then create jsonwebtokens.js in the mock directory.
We will be mocking the sendgrid mail module. To do this, we create a folder called sendgrid
inside of mocks, and a file called mail.js
inside that folder.
We then need to provide our own versions of what the module is giving us. Not completely, just enough code to where our tests do not fail because methods don’t exist. The functions we provide literally don’t need to do anything as long as that doesn’t affect the actual thing you are trying to test.
Testing file uploads
We will want to test the file upload for our user avatar. The user should be able to upload an avatar image and we need to validate that with supertest. When a file is uploaded, a buffer should be stored in the user file on the db.
To start, we will create a folder in our testing directory called fixtures
. Fixtures are items that our tests can use to validate tests. We will place an image here to use as our test image for the avatar.
Here is how that test looks:
test('Should upload avatar image', async () => {
await request(app)
.post('/users/me/avatar')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`)
.attach('avatar', 'tests/fixtures/profile-pic.jpg')
.expect(200)
let user = await User.findById(userOneId)
// here we check that the data stored at user.avatar IS indeed buffer data
// expect.any() takes a type that you want to check
expect(user.avatar).toEqual(expect.any(Buffer))
})
The following two tests test that the user can 1) Update valid user fields, and 2) NOT update invalid user fields.
test('Should update valid user fields', async () => {
await request(app)
.patch('/users/me')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`)
.send({
name: 'Jordan'
})
.expect(200)
let user = await User.findById(userOneId)
expect(user.name).toBe('Jordan')
})
test('Should not update invalid user fields', async () => {
await request(app)
.patch('/users/me')
.set('Authorization', `Bearer ${userOne.tokens[0].token}`)
.send({
location: 'Tennessee'
})
.expect(400)
})
To move on to the tasks test section of the course, we will have to do some refactoring of the code that creates our test user + test db.
We create a new file called db.js
in our fixtures
directory. We will move the code that created our user data into here, as we will need this for other testing suites we set up. We will setup the following code in that db file and export it for other testing suites to use, as well as using the function we create in their beforeEach()
calls.
const jwt = require('jsonwebtoken')
const mongoose = require('mongoose')
const User = require('../../src/models/user')
const userOneId = new mongoose.Types.ObjectId()
const userOne = {
_id: userOneId,
name: 'User One',
email: 'userOne@user.com',
password: '1qaz7ujm',
tokens: [{
token: jwt.sign({ _id: userOneId }, process.env.JWT_SECRET)
}]
}
const setupDatabase = async () => {
await User.deleteMany()
await new User(userOne).save()
}
module.exports = {
userOneId,
userOne,
setupDatabase
}
Now this isn’t all we have to do. Since Jest runs all of these test suites at the same time, each suite is trying to create and manipulate the database and this causes issues between the suites. To fix this, we just add a simple --runInBand
to the test script in our package.json
file. This will cause each test suite to run on its own.
Now we are free to create more tests in each suite without their before or after scripts affecting one another.
Back to blog...