- 1. JavaScript testing #1. Explaining types of tests. Basics of unit testing with Jest
- 2. JavaScript testing #2. Introducing Enzyme and testing React components
- 3. JavaScript testing #3. Testing props, the mount function and snapshot tests.
- 4. JavaScript testing #4. Mocking API calls and simulating React components interactions
- 5. JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux
- 6. JavaScript testing #6. Introduction to End-to-End testing with Cypress
- 7. JavaScript testing #7. Diving deeper into commands and selectors in Cypress
- 8. JavaScript testing #8. Integrating Cypress with Cucumber and Gherkin
- 9. JavaScript testing #9. Replacing Enzyme with React Testing Library
- 10. JavaScript testing #10. Advanced mocking with Jest and React Testing Library
- 11. JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks
- 12. JavaScript testing #12. Testing downloaded files and file inputs with Cypress
- 13. JavaScript testing #13. Mocking a REST API with the Mock Service Worker
- 14. JavaScript testing #14. Mocking WebSockets using the mock-socket library
- 15. JavaScript testing #15. Interpreting the code coverage metric
- 16. JavaScript testing #16. Snapshot testing with React, Jest, and Vitest
- 17. JavaScript testing #17. Introduction to End-to-End testing with Playwright
- 18. JavaScript testing #18. E2E Playwright tests for uploading and downloading files
Our applications sometimes include features that allow users to select files from their hard drives, and some functionalities might let users download files. Implementing End-to-End tests that ensure that everything works as expected might not seem straightforward at first.
In this article, we create a simple React application that lets users choose a JSON file from their machine and download a formatted version. Then, we use Playwright to test the file input and verify if the downloaded file is correct.
An application that formats JSON files
To start, let’s create a component with a file input.
JsonFormatter.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { useJsonFormatter } from './useJsonFormatter'; export const JsonFormatter = () => { const { handleJsonFile, error } = useJsonFormatter(); return ( <div style={{ display: 'flex', flexDirection: 'column', }} > <label htmlFor="json-file-input">Choose a valid JSON file</label> <input id="json-file-input" type="file" onChange={handleJsonFile} /> {error && ( <p data-testid="error-message" style={{ color: 'red' }}> {error} </p> )} </div> ); }; |
Under the hood, we extract the text from the selected file and parse it as JSON. Then, we format it and send it back to the user.
useJsonFormatter.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import { ChangeEvent, useState } from 'react'; export function useJsonFormatter() { const [error, setError] = useState<null | string>(null); const formatTextAsJson = (text: string) => { const data = JSON.parse(text); return JSON.stringify(data, null, 2); }; const downloadJson = (formattedJson: string, fileName: string) => { const anchor = document.createElement('a'); const blob = new Blob([formattedJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); anchor.setAttribute('href', url); anchor.setAttribute('download', `${fileName}_formatted`); anchor.click(); }; const handleJsonFile = async (event: ChangeEvent<HTMLInputElement>) => { const selectedFile = event.target.files?.[0]; setError(null); if (!selectedFile) { return; } try { const textFromFile = await selectedFile.text(); const formattedJson = formatTextAsJson(textFromFile); downloadJson(formattedJson, selectedFile.name); } catch { setError('The provided file does not contain valid JSON'); } event.target.value = ''; }; return { handleJsonFile, error, }; } |
If the user provides a valid JSON file, they get a formatted version with the same file name.
If the user selects an invalid file, we display the error message.
Preparing the necessary files
To test our application, we must prepare two files – one with valid JSON and one with invalid data. Let’s put them in the directory called resources in the tests folder.
tests/resources/data.json
1 |
{"key": "value"} |
tests/resources/invalidData.txt
1 |
{"key": "value |
Interacting with the file input
To interact with the file input through our End-to-End tests, we must first locate it on the page. One of the straightforward ways of doing that is to find it through the label text.
1 |
page.getByLabel('Choose a valid JSON file') |
Now, we must provide the path to our files with the setInputFiles function. One way is to provide a relative path to the file. The path will be resolved relative to the current working directory containing our package.json file.
1 2 3 |
await page .getByLabel('Choose a valid JSON file') .setInputFiles('./tests/resources/data.json'); |
We can use the above to test what happens when the user provides both valid and invalid files.
JsonFormatter.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import { expect, test } from '@playwright/test'; test.describe('When the user visits the page', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); }); test.describe('and provides an invalid file', () => { test.beforeEach(async ({ page }) => { await page .getByLabel('Choose a valid JSON file') .setInputFiles('./tests/resources/invalidData.txt'); }); test('it should display an error message', async ({ page }) => { await expect(page.getByTestId('error-message')).toBeVisible(); }); }); test.describe('and provides a valid file', () => { test.beforeEach(async ({ page }) => { await page .getByLabel('Choose a valid JSON file') .setInputFiles('./tests/resources/data.json'); }); test('it should not display an error message', async ({ page }) => { await expect(page.getByTestId('error-message')).not.toBeVisible(); }); }); }); |
To use page.goto('/') without providing the full URL we need to have the baseURL configuration set up. If you want to know more, check out JavaScript testing #17. Introduction to End-to-End testing with Playwright
Testing the name of the downloaded file
Every time the browser downloads a file, Playwright emits an event. To start listening for it, we need to call the waitForEvent function, which returns a promise.
1 2 3 4 5 6 7 8 |
let downloadedFile: Download; test.beforeEach(async ({ page }) => { const downloadedFilePromise = page.waitForEvent('download'); await page .getByLabel('Choose a valid JSON file') .setInputFiles('./tests/resources/data.json'); downloadedFile = await downloadedFilePromise; }); |
The crucial aspect is to start waiting for the event before the browser downloads the file, but don’t use the await keyword until it is downloaded.
Now, we can use the suggestedFilename function to test if the downloaded file’s name is correct.
JsonFormatter.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Download, expect, test } from '@playwright/test'; test.describe('When the user visits the page', () => { // ... test.describe('and provides a valid file', () => { let downloadedFile: Download; test.beforeEach(async ({ page }) => { const downloadedFilePromise = page.waitForEvent('download'); await page .getByLabel('Choose a valid JSON file') .setInputFiles('./tests/resources/data.json'); downloadedFile = await downloadedFilePromise; }); test('it should download a file with the correct name', async () => { const name = downloadedFile.suggestedFilename(); expect(name).toEqual('data.json'); }); // ... }); }); |
Checking the contents of the file
We also want to check if our application correctly formatted the JSON file. We need to use the createReadStream function that returns a read stream to do that.
If you want to know more about streams in Node.js, check out Node.js TypeScript #4. Paused and flowing modes of a readable stream
Playwright tests run in a Node.js environment. Thanks to that, we can use various APIs built into Node.js. The most straightforward way of reading the stream is to use the text function from node:stream/consumers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import { Download, expect, test } from '@playwright/test'; import { text } from 'node:stream/consumers'; test.describe('When the user visits the page', () => { // ... test.describe('and provides a valid file', () => { let downloadedFile: Download; test.beforeEach(async ({ page }) => { const downloadedFilePromise = page.waitForEvent('download'); await page .getByLabel('Choose a valid JSON file') .setInputFiles('./tests/resources/data.json'); downloadedFile = await downloadedFilePromise; }); test('it should download a file with the formatted JSON', async () => { const fileStream = await downloadedFile.createReadStream(); const result = await text(fileStream); expect(result).toEqual('{\n "key": "value"\n}'); }); // ... }); }); |
Summary
In this article, we’ve learned how to use Playwright to write End-to-End tests to verify features that include dealing with files. This included learning how to simulate choosing a file from the hard drive. Besides that, we also learned how to verify downloaded files by checking their names and content to ensure they contain the correct data. Fortunately, Playwright makes working with files very straightforward.