CEC-4499: add bulk update configs support (#357)
* add taskRunner util * add bulk update config flow
This commit is contained in:
@@ -7,19 +7,31 @@ import { Link } from "react-router-dom";
|
||||
import { Permissions } from "../../../utils/roles";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { VehicleProvider } from "../../Contexts/VehicleContext";
|
||||
import { VehicleProvider, VehicleContext } from "../../Contexts/VehicleContext";
|
||||
import CarSelectionTable from "../../Controls/CarSelectionTable";
|
||||
import OptionsDropdown from "../../Controls/OptionsDropdown";
|
||||
import { RoleWrap } from "../../Controls/RoleWrap";
|
||||
import SearchField from "../../Controls/SearchField";
|
||||
import DropDownButton from "../../Controls/DropDownButton";
|
||||
import TransformModal from "../../TransformModal";
|
||||
import useStyles from "../../useStyles";
|
||||
import TaskRunner from "../../../utils/taskRunner";
|
||||
|
||||
const MainForm = () => {
|
||||
const classes = useStyles();
|
||||
const [search, setSearch] = useState("");
|
||||
const [online, setOnline] = useState(false);
|
||||
const [onlineHMI, setOnlineHMI] = useState(false);
|
||||
const { setTitle, setSitePath } = useStatusContext();
|
||||
const [selectedVins, setSelectedVins] = useState([]);
|
||||
const [config, setConfig] = useState({
|
||||
force: {
|
||||
label: "Force push",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
})
|
||||
const [showUpdateConfigModal, setShowUpdateConfigModal] = useState(false);
|
||||
const { setTitle, setSitePath, setMessage } = useStatusContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
@@ -36,6 +48,45 @@ const MainForm = () => {
|
||||
setOnline(event.target.checked);
|
||||
};
|
||||
|
||||
const handleSelectAll = (cars) => {
|
||||
setSelectedVins(cars);
|
||||
};
|
||||
|
||||
const handleSelect = (event, key) => {
|
||||
setSelectedVins((selectedVins) => {
|
||||
if (event.target.checked) {
|
||||
return [...selectedVins, key];
|
||||
}
|
||||
return selectedVins.filter(vin => vin !== key);
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadConfig = (fn) => {
|
||||
const taskRunner = new TaskRunner(5);
|
||||
const request = (vin, i) => {
|
||||
const messagePrefix = `${i+1}/${selectedVins.length} "${vin}":`;
|
||||
return async () => {
|
||||
const result = await fn(vin, config.force.value, token)
|
||||
.then(() => {
|
||||
setMessage(`${messagePrefix} updated.`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setMessage(`${messagePrefix} ${error.message}`);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
selectedVins.forEach((vin, i) => taskRunner.push(request(vin, i)))
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Update Configs",
|
||||
disabled: selectedVins.length === 0,
|
||||
trigger: () => setShowUpdateConfigModal(true),
|
||||
},
|
||||
];
|
||||
|
||||
const handleOnlineHMI = (event) => {
|
||||
setOnlineHMI(event.target.checked);
|
||||
};
|
||||
@@ -49,7 +100,7 @@ const MainForm = () => {
|
||||
return (
|
||||
<div className={clsx(classes.paper, classes.tableSize)}>
|
||||
<Grid container className={classes.root} spacing={2}>
|
||||
<Grid item md={4} className={classes.textJustifyAlign}>
|
||||
<Grid item md={4} className={clsx(classes.textJustifyAlign, classes.actionsBar)}>
|
||||
<RoleWrap
|
||||
groups={groups}
|
||||
providers={providers}
|
||||
@@ -59,11 +110,12 @@ const MainForm = () => {
|
||||
<AddCircleIcon fontSize="large" />
|
||||
</Link>
|
||||
</RoleWrap>
|
||||
<DropDownButton actions={actions} payload={[selectedVins]} />
|
||||
</Grid>
|
||||
<Grid item md={4} className={classes.textCenterAlign}>
|
||||
<SearchField classes={classes} onSearch={handleSearch} />
|
||||
</Grid>
|
||||
<Grid item md={2} className={classes.textJustifyAlign}>
|
||||
<Grid item md={2} className={clsx(classes.textJustifyAlign, classes.actionsBar)}>
|
||||
<OptionsDropdown listId="filter-menu">
|
||||
<MenuItem>
|
||||
<FormControlLabel
|
||||
@@ -86,13 +138,29 @@ const MainForm = () => {
|
||||
<CarSelectionTable
|
||||
classes={classes}
|
||||
token={token}
|
||||
multiSelect={false}
|
||||
multiSelect
|
||||
search={{
|
||||
search,
|
||||
online: online ? true : null,
|
||||
online_hmi: onlineHMI ? true : null,
|
||||
}}
|
||||
selected={selectedVins}
|
||||
onSelect={handleSelect}
|
||||
onSelectAll={handleSelectAll}
|
||||
/>
|
||||
<VehicleContext.Consumer>
|
||||
{(context) => (
|
||||
<TransformModal
|
||||
open={showUpdateConfigModal}
|
||||
close={() => setShowUpdateConfigModal(false)}
|
||||
title="Update Configs"
|
||||
body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`}
|
||||
data={config}
|
||||
setData={setConfig}
|
||||
submit={() => handleUploadConfig(context.uploadConfig)}
|
||||
/>
|
||||
)}
|
||||
</VehicleContext.Consumer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { logger } from "../../services/monitoring";
|
||||
import api from "../../services/vehiclesAPI";
|
||||
import { validateVIN } from "../../utils/validationSupplier";
|
||||
|
||||
const VehicleContext = React.createContext();
|
||||
export const VehicleContext = React.createContext();
|
||||
|
||||
const validateAdd = (vehicle) => {
|
||||
if (vehicle == null) {
|
||||
@@ -299,7 +299,7 @@ export const VehicleProvider = ({ children }) => {
|
||||
updateVehicle,
|
||||
getFleets,
|
||||
getVersionLog,
|
||||
uploadConfig
|
||||
uploadConfig,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DownloadFileLink Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="choose action"
|
||||
class="MuiButtonGroup-root MuiButtonGroup-contained"
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-containedPrimary"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Action One
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-containedPrimary MuiButton-containedSizeSmall MuiButton-sizeSmall"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
|
||||
data-testid="ArrowDropDownIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="m7 10 5 5 5-5z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DropDownButton Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="choose action"
|
||||
class="MuiButtonGroup-root MuiButtonGroup-contained"
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-containedPrimary"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Action One
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-containedPrimary MuiButton-containedSizeSmall MuiButton-sizeSmall"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
|
||||
data-testid="ArrowDropDownIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="m7 10 5 5 5-5z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
109
src/components/Controls/DropDownButton/index.jsx
Normal file
109
src/components/Controls/DropDownButton/index.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ClickAwayListener,
|
||||
Grow,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Paper,
|
||||
Popper,
|
||||
} from "@mui/material";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
|
||||
const DropDownButton = ({ actions = [], payload = [] }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const anchorRef = useRef(null);
|
||||
|
||||
const handleClick = () => {
|
||||
actions[selectedIndex].trigger(...payload);
|
||||
};
|
||||
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
setSelectedIndex(index);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen(open => !open);
|
||||
};
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (
|
||||
anchorRef.current &&
|
||||
anchorRef.current.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup
|
||||
color="primary"
|
||||
variant="contained"
|
||||
ref={anchorRef}
|
||||
aria-label="choose action"
|
||||
>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
disabled={actions[selectedIndex].disabled}
|
||||
>
|
||||
{actions[selectedIndex].name}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
aria-controls={open ? 'split-button-menu' : undefined}
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
aria-label="select action"
|
||||
aria-haspopup="menu"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper
|
||||
sx={{
|
||||
zIndex: 100,
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin:
|
||||
placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="split-button-menu" autoFocusItem>
|
||||
{actions.map((action, index) => (
|
||||
<MenuItem
|
||||
key={action.name}
|
||||
disabled={actions[index].disabled}
|
||||
selected={index === selectedIndex}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{action.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DropDownButton;
|
||||
80
src/components/Controls/DropDownButton/index.test.jsx
Normal file
80
src/components/Controls/DropDownButton/index.test.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import { fireEvent, render, waitFor, screen } from "@testing-library/react";
|
||||
|
||||
import DropDownButton from ".";
|
||||
import addSnapshotSerializer from "../../../utils/snapshot";
|
||||
|
||||
describe("DropDownButton", () => {
|
||||
beforeAll(() => {
|
||||
addSnapshotSerializer(expect);
|
||||
});
|
||||
|
||||
it("Render", async () => {
|
||||
const actions = [
|
||||
{
|
||||
name: "Action One",
|
||||
disabled: false,
|
||||
trigger: (paramOne, paramTwo) => {}
|
||||
},
|
||||
{
|
||||
name: "Action Two",
|
||||
disabled: false,
|
||||
trigger: (paramOne, paramTwo) => {}
|
||||
},
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<DropDownButton
|
||||
actions={actions}
|
||||
payload={["somePayload", "someOtherPayload"]}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("properly disables an action", async () => {
|
||||
const actions = [
|
||||
{
|
||||
name: "Disabled Action",
|
||||
disabled: true,
|
||||
trigger: () => {}
|
||||
},
|
||||
];
|
||||
|
||||
const { getByText } = render(
|
||||
<DropDownButton
|
||||
actions={actions}
|
||||
payload={[]}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
const buttonEl = getByText("Disabled Action").parentElement;
|
||||
expect(buttonEl).toHaveProperty("disabled", true);
|
||||
});
|
||||
|
||||
it("properly passes payload to callback", async () => {
|
||||
const actions = [
|
||||
{
|
||||
name: "Action One",
|
||||
disabled: false,
|
||||
trigger: jest.fn(),
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<DropDownButton
|
||||
actions={actions}
|
||||
payload={["somePayload", "somePayload2"]}
|
||||
/>
|
||||
);
|
||||
|
||||
const buttonEl = screen.getByText("Action One");
|
||||
fireEvent.click(buttonEl);
|
||||
expect(actions[0].trigger).toHaveBeenCalledWith("somePayload", "somePayload2");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TransformModal Render 1`] = `
|
||||
<div
|
||||
aria-hidden="true"
|
||||
/>
|
||||
`;
|
||||
92
src/components/TransformModal/index.jsx
Normal file
92
src/components/TransformModal/index.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
|
||||
const TransformModal = ({
|
||||
open,
|
||||
close,
|
||||
title,
|
||||
body,
|
||||
data,
|
||||
setData,
|
||||
submit
|
||||
}) => {
|
||||
const handleClick = () => {
|
||||
close();
|
||||
submit();
|
||||
};
|
||||
|
||||
const handleChange = (key) => {
|
||||
setData((data) => {
|
||||
const {[key]: toChange, ...rest} = data;
|
||||
toChange.value = !toChange.value;
|
||||
return {
|
||||
[key]: toChange,
|
||||
...rest
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={close}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{body && <DialogContentText>
|
||||
{body}
|
||||
</DialogContentText>}
|
||||
<FormGroup>
|
||||
{Object.entries(data).map((([key, value]) => {
|
||||
switch (value.type) {
|
||||
case "boolean":
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
label={value.label}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data[key].value}
|
||||
onChange={() => handleChange(key)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}))}
|
||||
</FormGroup>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={close}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={handleClick}
|
||||
autoFocus
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default TransformModal;
|
||||
56
src/components/TransformModal/index.test.jsx
Normal file
56
src/components/TransformModal/index.test.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
|
||||
import TransformModal from ".";
|
||||
import addSnapshotSerializer from "../../utils/snapshot";
|
||||
|
||||
const data = {
|
||||
test: {
|
||||
label: "Test field",
|
||||
value: false,
|
||||
type: "boolean",
|
||||
},
|
||||
}
|
||||
|
||||
describe("TransformModal", () => {
|
||||
beforeAll(() => {
|
||||
addSnapshotSerializer(expect);
|
||||
});
|
||||
|
||||
it("Render", async () => {
|
||||
|
||||
const { container } = render(
|
||||
<TransformModal
|
||||
open={true}
|
||||
close={() => {}}
|
||||
title="Title"
|
||||
body="Body"
|
||||
data={data}
|
||||
setData={() => {}}
|
||||
submit={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("properly renders a checkbox for a boolean", async () => {
|
||||
const { getByText } = render(
|
||||
<TransformModal
|
||||
open={true}
|
||||
close={() => {}}
|
||||
title="Title"
|
||||
body="Body"
|
||||
data={data}
|
||||
setData={() => {}}
|
||||
submit={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(getByText("Test field")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -302,6 +302,11 @@ const useStyles = makeStyles((theme) => ({
|
||||
width: "100%",
|
||||
padding: "16px 0",
|
||||
backgroundColor: "#fafafa",
|
||||
},
|
||||
actionsBar: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
36
src/utils/taskRunner.js
Normal file
36
src/utils/taskRunner.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export default class TaskRunner {
|
||||
constructor(concurrencyLimit = 1) {
|
||||
this.queue = [];
|
||||
this.running = 0;
|
||||
this.concurrencyLimit = concurrencyLimit;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (this.running >= this.concurrencyLimit || this.queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const task = this.queue.shift();
|
||||
this.running += 1;
|
||||
task();
|
||||
}
|
||||
|
||||
async push(fn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const task = async () => {
|
||||
try {
|
||||
const result = await fn();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
this.running -= 1;
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
this.queue.push(task);
|
||||
this.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
58
src/utils/taskRunner.test.js
Normal file
58
src/utils/taskRunner.test.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import TaskRunner from "./taskRunner";
|
||||
|
||||
const mockPromise = async (id, ms) => {
|
||||
await new Promise(resolve => setTimeout(resolve, ms));
|
||||
return id;
|
||||
}
|
||||
|
||||
const asyncFn1 = () => mockPromise(1, 200);
|
||||
const asyncFn2 = () => mockPromise(2, 100);
|
||||
const asyncFn3 = () => mockPromise(3, 50);
|
||||
|
||||
describe("TaskRunner", () => {
|
||||
it("runs task added to queue, when space available", () => {
|
||||
const taskRunner = new TaskRunner(2);
|
||||
expect(taskRunner.running).toEqual(0);
|
||||
taskRunner.push(() => mockPromise(1, 300));
|
||||
expect(taskRunner.running).toEqual(1);
|
||||
});
|
||||
|
||||
it("keeps task in queue when at concurrency limit", () => {
|
||||
const taskRunner = new TaskRunner(2);
|
||||
expect(taskRunner.running).toEqual(0);
|
||||
taskRunner.push(() => mockPromise(1, 100));
|
||||
taskRunner.push(() => mockPromise(2, 25));
|
||||
taskRunner.push(() => mockPromise(3, 10));
|
||||
expect(taskRunner.running).toEqual(2);
|
||||
expect(taskRunner.queue.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("runs queued tasks as space becomes available", async () => {
|
||||
const taskRunner = new TaskRunner(2);
|
||||
taskRunner.push(() => mockPromise(1, 600));
|
||||
taskRunner.push(() => mockPromise(2, 300));
|
||||
taskRunner.push(() => mockPromise(3, 100));
|
||||
expect(taskRunner.queue.length).toEqual(1);
|
||||
await new Promise(r => setTimeout(r, 301));
|
||||
expect(taskRunner.queue.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("runs tasks in order", async () => {
|
||||
const actual = [];
|
||||
const taskRunner = new TaskRunner(2);
|
||||
taskRunner.push(asyncFn1)
|
||||
.then((id) => {
|
||||
actual.push(id);
|
||||
});
|
||||
taskRunner.push(asyncFn2)
|
||||
.then((id) => {
|
||||
actual.push(id);
|
||||
});
|
||||
taskRunner.push(asyncFn3)
|
||||
.then((id) => {
|
||||
actual.push(id);
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
expect(actual).toEqual([2, 3, 1]);
|
||||
});
|
||||
})
|
||||
Reference in New Issue
Block a user