Change main UI layout and add VINs to add and upload forms (#16)
* Add new upload update package form Add new add vehicle form Add new side menu layout Add new toolbar layout Update and add unit tests * Enable add get and add vehicles * Integration issues with ota_update service * Update get vehicle JSON format * Fix related unit test Add release notes field * Add StatusContext to display error and status messages
This commit is contained in:
@@ -9,6 +9,7 @@ export const FileUploadProvider = ({ children }) => {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [cancelUpload, setCancelUpload] = useState(null);
|
||||
const [linkURL, setLinkURL] = useState(null);
|
||||
const [files, setFiles] = useState(null);
|
||||
|
||||
const done = () => {
|
||||
setCancelUpload(null);
|
||||
@@ -24,15 +25,37 @@ export const FileUploadProvider = ({ children }) => {
|
||||
done();
|
||||
};
|
||||
|
||||
const upload = async (files, accessToken) => {
|
||||
const validateUpload = (formData, accessToken, uploadFiles) => {
|
||||
if (!formData) {
|
||||
throw new Error("Missing package update data");
|
||||
}
|
||||
|
||||
if (!formData.packagename || formData.packagename.length === 0) {
|
||||
throw new Error("Package name required");
|
||||
}
|
||||
|
||||
if (!formData.version || formData.version.length === 0) {
|
||||
throw new Error("Package update version required");
|
||||
}
|
||||
|
||||
if (!formData.vehicles || formData.vehicles.length === 0) {
|
||||
throw new Error("Vehicles required");
|
||||
}
|
||||
|
||||
if (!uploadFiles || uploadFiles.length === 0) {
|
||||
throw new Error("File required");
|
||||
}
|
||||
|
||||
if (!accessToken || accessToken.length === 0) {
|
||||
throw new Error("Access token required");
|
||||
}
|
||||
};
|
||||
|
||||
const upload = async (formData, accessToken, uploadFiles) => {
|
||||
validateUpload(formData, accessToken, uploadFiles);
|
||||
|
||||
try {
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error("File required");
|
||||
}
|
||||
if (!accessToken || accessToken.length === 0) {
|
||||
throw new Error("Access token required");
|
||||
}
|
||||
const file = files[0].file;
|
||||
const file = uploadFiles[0];
|
||||
const filename = file.name;
|
||||
|
||||
setUploading(true);
|
||||
@@ -43,6 +66,7 @@ export const FileUploadProvider = ({ children }) => {
|
||||
|
||||
const { data } = await uploadFile(
|
||||
file,
|
||||
formData,
|
||||
accessToken,
|
||||
setProgress,
|
||||
cancelUpload
|
||||
@@ -56,18 +80,12 @@ export const FileUploadProvider = ({ children }) => {
|
||||
setCancelUpload(null);
|
||||
setProgress(100);
|
||||
} catch (e) {
|
||||
setUploading(true);
|
||||
setStatus(`Error occured: ${e.message}`);
|
||||
setProgress(-1);
|
||||
}
|
||||
};
|
||||
|
||||
const rejectedFile = (files) => {
|
||||
if (files.length === 0) return;
|
||||
setUploading(true);
|
||||
setStatus(`Rejected ${files[0].name} too large`);
|
||||
setProgress(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<FileUploadContext.Provider
|
||||
value={{
|
||||
@@ -75,9 +93,10 @@ export const FileUploadProvider = ({ children }) => {
|
||||
progress,
|
||||
status,
|
||||
linkURL,
|
||||
files,
|
||||
upload,
|
||||
cancel,
|
||||
rejectedFile,
|
||||
setFiles,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
jest.mock("../../services/uploadFile");
|
||||
|
||||
import { setUploadFileDelay } from "../../services/uploadFile";
|
||||
import {
|
||||
FileUploadProvider,
|
||||
useFileUploadContext,
|
||||
} from "../Contexts/FileUploadContext";
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
@@ -13,11 +8,19 @@ import {
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
|
||||
const checkState = (uploading, progress, status, linkURL) => {
|
||||
import { setUploadFileDelay } from "../../services/uploadFile";
|
||||
import {
|
||||
FileUploadProvider,
|
||||
useFileUploadContext,
|
||||
} from "../Contexts/FileUploadContext";
|
||||
import { StatusProvider, useStatusContext } from "../Contexts/StatusContext";
|
||||
|
||||
const checkState = (uploading, progress, status, linkURL, message) => {
|
||||
expect(screen.getByTestId("uploading").innerHTML).toEqual(uploading);
|
||||
expect(screen.getByTestId("progress").innerHTML).toEqual(progress);
|
||||
expect(screen.getByTestId("status").innerHTML).toEqual(status);
|
||||
expect(screen.getByTestId("linkURL").innerHTML).toEqual(linkURL);
|
||||
expect(screen.getByTestId("message").innerHTML).toEqual(message);
|
||||
};
|
||||
|
||||
describe("FileUploadContext", () => {
|
||||
@@ -30,32 +33,63 @@ describe("FileUploadContext", () => {
|
||||
linkURL,
|
||||
upload,
|
||||
cancel,
|
||||
setFiles,
|
||||
} = useFileUploadContext();
|
||||
const TEST_FILE = [{ file: { name: "test.jpg", size: 0 } }];
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const TEST_FILE = [{ name: "test.jpg", size: 0 }];
|
||||
const TEST_ACCESSTOKEN = "ACCESSTOKEN";
|
||||
const TEST_FORMDATA = {
|
||||
packagename: "TEST",
|
||||
version: "VERSION",
|
||||
vehicles: ["VIN"],
|
||||
};
|
||||
const exec = async (form, token, file) => {
|
||||
try {
|
||||
await upload(form, token, file);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="uploading">{uploading.toString()}</div>
|
||||
<div data-testid="progress">{progress.toString()}</div>
|
||||
<div data-testid="status">{status}</div>
|
||||
<div data-testid="message">{message}</div>
|
||||
<div data-testid="linkURL">{linkURL}</div>
|
||||
<button data-testid="uploadNoFile" onClick={() => upload()} />
|
||||
<button
|
||||
data-testid="uploadNoFile"
|
||||
onClick={() => {
|
||||
exec(TEST_FORMDATA, TEST_ACCESSTOKEN, null);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
data-testid="uploadNoToken"
|
||||
onClick={() => upload(TEST_FILE)}
|
||||
onClick={() => {
|
||||
exec(TEST_FORMDATA, null, TEST_FILE);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
data-testid="uploadNoFormData"
|
||||
onClick={() => {
|
||||
exec({}, TEST_ACCESSTOKEN, TEST_FILE);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
data-testid="upload"
|
||||
onClick={() => upload(TEST_FILE, TEST_ACCESSTOKEN)}
|
||||
onClick={() => exec(TEST_FORMDATA, TEST_ACCESSTOKEN, TEST_FILE)}
|
||||
/>
|
||||
<button data-testid="cancel" onClick={() => cancel()} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<FileUploadProvider>
|
||||
<TestComp />
|
||||
</FileUploadProvider>
|
||||
<StatusProvider>
|
||||
<FileUploadProvider>
|
||||
<TestComp />
|
||||
</FileUploadProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,17 +98,31 @@ describe("FileUploadContext", () => {
|
||||
});
|
||||
|
||||
it("Initial state", async () => {
|
||||
checkState("false", "0", "", "");
|
||||
checkState("false", "0", "", "", "");
|
||||
});
|
||||
|
||||
it("Upload no file", async () => {
|
||||
fireEvent.click(screen.getByTestId("uploadNoFile"));
|
||||
checkState("false", "-1", "Error occured: File required", "");
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("message").innerHTML).not.toBe("")
|
||||
);
|
||||
checkState("false", "0", "", "", "File required");
|
||||
});
|
||||
|
||||
it("Upload no access token", async () => {
|
||||
fireEvent.click(screen.getByTestId("uploadNoToken"));
|
||||
checkState("false", "-1", "Error occured: Access token required", "");
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("message").innerHTML).not.toBe("")
|
||||
);
|
||||
checkState("false", "0", "", "", "Access token required");
|
||||
});
|
||||
|
||||
it("Upload no form data", async () => {
|
||||
fireEvent.click(screen.getByTestId("uploadNoFormData"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("message").innerHTML).not.toBe("")
|
||||
);
|
||||
checkState("false", "0", "", "", "Package name required");
|
||||
});
|
||||
|
||||
it("Upload file", async () => {
|
||||
@@ -82,7 +130,7 @@ describe("FileUploadContext", () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("progress").innerHTML).toEqual("100")
|
||||
);
|
||||
checkState("true", "100", "Uploaded test.jpg", "CLOUDFRONT_URL");
|
||||
checkState("true", "100", "Uploaded test.jpg", "CLOUDFRONT_URL", "");
|
||||
});
|
||||
|
||||
it("Cancel upload", async () => {
|
||||
@@ -91,12 +139,12 @@ describe("FileUploadContext", () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("progress").innerHTML).toEqual("50")
|
||||
);
|
||||
checkState("true", "50", "Uploading test.jpg", "");
|
||||
checkState("true", "50", "Uploading test.jpg", "", "");
|
||||
|
||||
fireEvent.click(screen.getByTestId("cancel"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("progress").innerHTML).toEqual("0")
|
||||
);
|
||||
checkState("false", "0", "Upload cancelled", "");
|
||||
checkState("false", "0", "Upload cancelled", "", "");
|
||||
});
|
||||
});
|
||||
|
||||
20
src/components/Contexts/StatusContext.jsx
Normal file
20
src/components/Contexts/StatusContext.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
|
||||
const StatusContext = React.createContext();
|
||||
|
||||
export const StatusProvider = ({ children }) => {
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
return (
|
||||
<StatusContext.Provider
|
||||
value={{
|
||||
message,
|
||||
setMessage,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StatusContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useStatusContext = () => useContext(StatusContext);
|
||||
@@ -34,13 +34,13 @@ export const UserProvider = ({ children }) => {
|
||||
if (!token || !token.refreshToken || !token.refreshToken.token) return null;
|
||||
const result = await refresh(token.refreshToken.token);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const isError = (resp) => {
|
||||
if (resp === null) return true;
|
||||
if (resp && resp.error) return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const startSessionTimer = () => {
|
||||
const duration = 1000 * token.idToken.payload.exp - new Date().getTime();
|
||||
@@ -62,7 +62,7 @@ export const UserProvider = ({ children }) => {
|
||||
const result = await auth.verify(idToken);
|
||||
|
||||
if (
|
||||
!result.authenticated ||
|
||||
(!result.valid && !result.authenticated) ||
|
||||
!token.idToken.payload ||
|
||||
!token.idToken.payload.exp
|
||||
) {
|
||||
@@ -71,10 +71,9 @@ export const UserProvider = ({ children }) => {
|
||||
signOut();
|
||||
return;
|
||||
}
|
||||
|
||||
startSessionTimer();
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
startSessionTimer();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,14 +26,14 @@ const INVALID_TOKEN_RESPONSE = {
|
||||
message: "Bad Request Message",
|
||||
};
|
||||
|
||||
const setupRefreshEnv = (refreshResponse, authenticated) => {
|
||||
const setupRefreshEnv = (refreshResponse, valid) => {
|
||||
auth.setRefreshResponse(refreshResponse);
|
||||
auth.setVerifyResponse({ authenticated });
|
||||
auth.setVerifyResponse({ valid });
|
||||
};
|
||||
|
||||
const setupSignInEnv = (refreshResponse, authenticated) => {
|
||||
const setupSignInEnv = (refreshResponse, valid) => {
|
||||
auth.setSignInResponse(refreshResponse);
|
||||
auth.setVerifyResponse({ authenticated });
|
||||
auth.setVerifyResponse({ valid });
|
||||
};
|
||||
|
||||
const checkBaseResults = (error, fetching, token) => {
|
||||
@@ -57,12 +57,7 @@ describe("UseContext", () => {
|
||||
describe("Signin", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const {
|
||||
signIn,
|
||||
error,
|
||||
token,
|
||||
fetching,
|
||||
} = useUserContext();
|
||||
const { signIn, error, token, fetching } = useUserContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -171,12 +166,7 @@ describe("UseContext", () => {
|
||||
describe("Refresh", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const {
|
||||
refresh,
|
||||
error,
|
||||
token,
|
||||
fetching,
|
||||
} = useUserContext();
|
||||
const { refresh, error, token, fetching } = useUserContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -188,7 +178,10 @@ describe("UseContext", () => {
|
||||
data-testid="refreshInvalidToken"
|
||||
onClick={() => refresh("INVALID_TOKEN")}
|
||||
/>
|
||||
<button data-testid="refreshValidToken" onClick={() => refresh("TEST_TOKEN")} />
|
||||
<button
|
||||
data-testid="refreshValidToken"
|
||||
onClick={() => refresh("TEST_TOKEN")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
60
src/components/Contexts/VehicleContext.jsx
Normal file
60
src/components/Contexts/VehicleContext.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import api from "../../services/vehicles";
|
||||
|
||||
const VehicleContext = React.createContext();
|
||||
|
||||
const validateAdd = (vehicle) => {
|
||||
if (vehicle === null) {
|
||||
throw new Error("No vehicle data");
|
||||
}
|
||||
|
||||
if (!vehicle.vin || vehicle.vin.length === 0) {
|
||||
throw new Error("VIN required");
|
||||
}
|
||||
|
||||
if (vehicle.vin.length > 17) {
|
||||
throw new Error("VIN cannot be larger than 17 characters");
|
||||
}
|
||||
};
|
||||
|
||||
export const VehicleProvider = ({ children }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [vehicles, setVehicles] = useState([]);
|
||||
|
||||
const getVehicles = async (search, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
const {
|
||||
data: { data },
|
||||
} = await api.getVehicles(search, token);
|
||||
setVehicles(data);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addVehicle = async (vehicle, token) => {
|
||||
try {
|
||||
setBusy(true);
|
||||
validateAdd(vehicle);
|
||||
await api.addVehicle(vehicle, token);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VehicleContext.Provider
|
||||
value={{
|
||||
busy,
|
||||
vehicles,
|
||||
getVehicles,
|
||||
addVehicle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</VehicleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useVehicleContext = () => useContext(VehicleContext);
|
||||
142
src/components/Contexts/VehicleContext.test.jsx
Normal file
142
src/components/Contexts/VehicleContext.test.jsx
Normal file
@@ -0,0 +1,142 @@
|
||||
jest.mock("../../services/vehicles");
|
||||
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { VehicleProvider, useVehicleContext } from "./VehicleContext";
|
||||
import { StatusProvider, useStatusContext } from "./StatusContext";
|
||||
|
||||
const checkVehicleResults = (error, busy, vehicles) => {
|
||||
checkBaseResults(error, busy);
|
||||
expect(screen.getByTestId("vehicles").innerHTML).toEqual(vehicles);
|
||||
};
|
||||
|
||||
const checkBaseResults = (error, busy) => {
|
||||
expect(screen.getByTestId("error").innerHTML).toEqual(error);
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
|
||||
};
|
||||
|
||||
describe("VehicleContext", () => {
|
||||
describe("getVehicles", () => {
|
||||
beforeEach(() => {
|
||||
const TestComp = () => {
|
||||
const { busy, error, vehicles, getVehicles } = useVehicleContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{error}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<div data-testid="vehicles">{JSON.stringify(vehicles)}</div>
|
||||
<button
|
||||
data-testid="getVehicles"
|
||||
onClick={() => getVehicles(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<VehicleProvider>
|
||||
<TestComp />
|
||||
</VehicleProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("Initial state", () => {
|
||||
checkVehicleResults("", "false", "[]");
|
||||
});
|
||||
|
||||
it("getVehicles", async () => {
|
||||
fireEvent.click(screen.getByTestId("getVehicles"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("vehicles").innerHTML).not.toBe("[]")
|
||||
);
|
||||
checkVehicleResults("", "false", JSON.stringify(expectedVehicleData));
|
||||
});
|
||||
});
|
||||
|
||||
describe("AddVehicles", () => {
|
||||
beforeEach(async () => {
|
||||
const TestComp = () => {
|
||||
const { busy, addVehicle } = useVehicleContext();
|
||||
const { message, setMessage } = useStatusContext();
|
||||
const add = async (data) => {
|
||||
try {
|
||||
await addVehicle(data);
|
||||
} catch (e) {
|
||||
setMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="error">{message}</div>
|
||||
<div data-testid="busy">{busy.toString()}</div>
|
||||
<button data-testid="addVehiclesNull" onClick={() => add(null)} />
|
||||
<button data-testid="addVehiclesNoVIN" onClick={() => add({})} />
|
||||
<button
|
||||
data-testid="addVehicles"
|
||||
onClick={() => add({ vin: "XXXXXXXXXXX" })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
render(
|
||||
<StatusProvider>
|
||||
<VehicleProvider>
|
||||
<TestComp />
|
||||
</VehicleProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("Initial state", () => {
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
|
||||
it("addVehiclesNull", async () => {
|
||||
fireEvent.click(screen.getByTestId("addVehiclesNull"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("No vehicle data", "false");
|
||||
});
|
||||
|
||||
it("addVehiclesNoVIN", async () => {
|
||||
fireEvent.click(screen.getByTestId("addVehiclesNoVIN"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("VIN required", "false");
|
||||
});
|
||||
|
||||
it("addVehicles", async () => {
|
||||
fireEvent.click(screen.getByTestId("addVehicles"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||
);
|
||||
checkBaseResults("", "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const expectedVehicleData = [
|
||||
{ vin: "3C4PDCBG0ET127145" },
|
||||
{ vin: "1G1FP87S3GN100062" },
|
||||
{ vin: "1HGCG325XYA062256" },
|
||||
{ vin: "1J4GZ78YXWC160024" },
|
||||
{ vin: "2C3CCAAG8CH222800" },
|
||||
{ vin: "KNADM4A39C6028108" },
|
||||
{ vin: "1G11C5SL9FF153507" },
|
||||
];
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
let uploading = false;
|
||||
let progress = 0;
|
||||
let status = null;
|
||||
let files = null;
|
||||
|
||||
export const FileUploadProvider = ({ children }) => {
|
||||
return <div data-testid="mocked-fileuploadprovider">{children}</div>;
|
||||
@@ -12,6 +13,10 @@ export const useFileUploadContext = () => ({
|
||||
uploading,
|
||||
progress,
|
||||
status,
|
||||
files,
|
||||
upload: jest.fn(),
|
||||
cancel: jest.fn(),
|
||||
setFiles: jest.fn((value) => {
|
||||
files = value;
|
||||
}),
|
||||
});
|
||||
|
||||
24
src/components/Contexts/__mocks__/VehicleContext.jsx
Normal file
24
src/components/Contexts/__mocks__/VehicleContext.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
|
||||
let busy = false;
|
||||
let vehicles = [];
|
||||
let error = null;
|
||||
|
||||
export const VehicleProvider = ({ children }) => {
|
||||
return <div data-testid="mocked-vehicleprovider">{children}</div>;
|
||||
};
|
||||
|
||||
export const useVehicleContext = () => ({
|
||||
busy,
|
||||
vehicles,
|
||||
getVehicles: jest.fn(() => vehicles),
|
||||
addVehicle: jest.fn(),
|
||||
});
|
||||
|
||||
export const setBusy = (val) => {
|
||||
busy = val;
|
||||
};
|
||||
|
||||
export const setVehicles = (val) => {
|
||||
vehicles = val;
|
||||
};
|
||||
Reference in New Issue
Block a user