diff --git a/front-end/app/global.scss b/front-end/app/global.scss index 8ba3c1f0..b6c390c1 100644 --- a/front-end/app/global.scss +++ b/front-end/app/global.scss @@ -26,6 +26,12 @@ padding-left: 0.5rem ; } +@media (min-width: 64em) { + .usa-alert--info .usa-alert__body { + padding-right: 3.5rem; + } +} + @media (min-width: 64em) { .usa-header--basic .usa-navbar { position: relative; @@ -33,6 +39,12 @@ } } +// removing the border on the left +.usa-form-group--error { + border-left-style: none !important; + padding-left: 1.2rem !important; +} + .footer-primary { div{ diff --git a/front-end/app/upload_file/page.tsx b/front-end/app/upload_file/page.tsx index 38f2c62d..5f58cbdf 100644 --- a/front-end/app/upload_file/page.tsx +++ b/front-end/app/upload_file/page.tsx @@ -1,120 +1,146 @@ 'use client' -import { FileInput, FormGroup, Button } from '@trussworks/react-uswds' +import { FileInput, FormGroup, Button, ErrorMessage } from '@trussworks/react-uswds' import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import LinkAccordion from '@/components/LinkAccordion/LinkAccordion'; -import {formatData, ProgressData, createWebSocket, stepHtml, alertHtml} from './utils' +import { formatData, ProgressData, createWebSocket, stepHtml, alertHtml } from './utils' import { useData } from '@/utils/DataContext'; export default function UploadFile() { - const { setData } = useData(); - const router = useRouter(); - const url = process.env.NEXT_PUBLIC_PROCESS_URL - const [progress, setProgress] = useState(null); // State for progress - const [socket, setSocket] = useState(null); - const [file, setFile] = useState(null); + const { setData } = useData(); + const router = useRouter(); + const url = process.env.NEXT_PUBLIC_PROCESS_URL + const [progress, setProgress] = useState(null); // State for progress + const [socket, setSocket] = useState(null); + const [file, setFile] = useState(null); + const [fileError, setFileError] = useState(null); - const handleSubmit = () => { - // Send form data to the server via a WebSocket - if(!file || !socket){ - return 'false'; + const fileErrors = { + size: "We can only accept .zip files smaller than 1GB", + type: "We can only accept .zip files" } - const formData = new FormData(); - formData.append("file", file); - socket.send(file) - }; - const addFile = (event: React.ChangeEvent) => { - const selectedFile = event.target.files?.item(0); - if(selectedFile){ - setFile(selectedFile); - } - }; + const fileSizeLimit = 1000000000; // 1 GB - useEffect(() => { - const ws = createWebSocket(url); - ws.onmessage = (event) => { - let data = formatData(event.data) - if(data.complete && data["processed_values"]){ - setData(data) - } else { - setProgress(formatData(event.data)); - } + const handleSubmit = () => { + // Send form data to the server via a WebSocket + if (!file || !socket) { + return 'false'; + } + const formData = new FormData(); + formData.append("file", file); + socket.send(file) }; - - ws.onclose = (event) => { - // Handle WebSocket closed + const addFile = (event: React.ChangeEvent) => { + const selectedFile = event.target.files?.item(0); + if (selectedFile?.size && selectedFile.size > fileSizeLimit) { + setFileError(fileErrors.size) + } + else if (!selectedFile?.name.toLowerCase().endsWith('.zip')) { + setFileError(fileErrors.type) + } + else if (selectedFile) { + setFileError(null); + setFile(selectedFile); + } }; - setSocket(ws); + useEffect(() => { + const ws = createWebSocket(url); + ws.onmessage = (event) => { + let data = formatData(event.data) + if (data.complete && data["processed_values"]) { + setData(data) + } else { + setProgress(formatData(event.data)); + } + }; - return () => { - ws.close(); // Close the WebSocket when the component unmounts - }; - }, []); + ws.onclose = (event) => { + // Handle WebSocket closed + }; + + setSocket(ws); - + return () => { + ws.close(); // Close the WebSocket when the component unmounts + }; + }, []); - const progressHtml = () =>{ - if(!progress || !file){ - return (<>) + const progressComponent = () => { + if (!progress || !file) { + return (<>) + } + return ( +
+
+

Processing your eCR

+

+ View the progress of your eCR through our pipeline +

+ {alertHtml(progress, file)} +
+
    + {stepHtml(progress)} +
+
+
+ + +
+
+
+ ) } - return ( -
-
-

Processing your eCR

-

- View the progress of your eCR through our pipeline -

- {alertHtml(progress, file)} -
-
    - {stepHtml(progress)} -
-
-
- - -
-
-
- ) - } - if(progress){ - return progressHtml() + if (progress) { + return progressComponent() } else { - return ( -
-
-

Upload your eCR

-

Select an eCR .zip file to process

-
-
-

- This tool is only for test data. Please do not upload patient data to this site. -

+ return ( +
+
+

Upload your eCR

+

Select an eCR .zip file to process

+
+
+

+ This tool is only for test data. Please do not upload patient data to this site. +

+
+ + {fileError && + + {fileError} + + } + + + +
+ +
+ +
- - -
- -
- -
-
- ) + ) } } diff --git a/front-end/tests/upload.test.js b/front-end/tests/upload.test.js index 48700bea..271d09a2 100644 --- a/front-end/tests/upload.test.js +++ b/front-end/tests/upload.test.js @@ -8,9 +8,9 @@ import { MockWebSocket } from "./mockWebSocket"; import { act } from "react-dom/test-utils"; let wsInstance = new MockWebSocket('ws://example.com'); -jest.mock("../app/upload_file/utils",()=>({ - ...(jest.requireActual('../app/upload_file/utils')), - createWebSocket: jest.fn(()=> { +jest.mock("../app/upload_file/utils", () => ({ + ...(jest.requireActual('../app/upload_file/utils')), + createWebSocket: jest.fn(() => { return wsInstance }) })); @@ -57,7 +57,7 @@ describe('UploadFile Component', () => { json: () => Promise.resolve({ success: true }), }) ); - + const sendSpy = jest.spyOn(wsInstance, 'send'); @@ -69,8 +69,8 @@ describe('UploadFile Component', () => { ) ); const fileInput = queryByTestId('file-input-input') - const file = new File(['{"test": "content"}'], 'test.json', { type: 'text/json' }); - const fileList = createFileList([file]); + const file = new File(['{"test": "content"}'], 'test.zip', { type: 'text/json' }); + const fileList = createFileList([file]); await act(async () => { fireEvent.change(fileInput, { @@ -92,4 +92,75 @@ describe('UploadFile Component', () => { global.fetch.mockRestore(); sendSpy.mockRestore(); }); + + it('should reject files that are too big', async () => { + // Create a function to mimic a FileList object + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ success: true }), + }) + ); + + + const sendSpy = jest.spyOn(wsInstance, 'send'); + + const { getByText, queryByTestId } = await act(async () => render + ( + + + + ) + ); + const fileInput = queryByTestId('file-input-input') + const file = new File(['{"test": "content"}'], 'test.zip', { type: 'text/json' }); + Object.defineProperty(file, 'size', { value: 1000000000 + 1 }) + const fileList = createFileList([file]); + + await act(async () => { + fireEvent.change(fileInput, { + target: { files: fileList }, + }); + }); + + expect(screen.getByText('We can only accept .zip files smaller than 1GB')).toBeInTheDocument(); + + // Reset fetch mock + global.fetch.mockRestore(); + sendSpy.mockRestore(); + }); + + it('should reject files that are not .zip', async () => { + // Create a function to mimic a FileList object + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ success: true }), + }) + ); + + + const sendSpy = jest.spyOn(wsInstance, 'send'); + + const { getByText, queryByTestId } = await act(async () => render + ( + + + + ) + ); + const fileInput = queryByTestId('file-input-input') + const file = new File(['{"test": "content"}'], 'test.json', { type: 'text/json' }); + const fileList = createFileList([file]); + + await act(async () => { + fireEvent.change(fileInput, { + target: { files: fileList }, + }); + }); + + expect(screen.getByText('We can only accept .zip files')).toBeInTheDocument(); + + // Reset fetch mock + global.fetch.mockRestore(); + sendSpy.mockRestore(); + }); });