Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/components/DataRetentionSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import DataRetentionSelector, {
TimeUnit,
DataRetentionValue,
} from "./DataRetentionSelector";

describe("DataRetentionSelector", () => {
const defaultValue: DataRetentionValue = {
value: 30,
unit: TimeUnit.DAYS,
};

test("renders with default value", () => {
const onChange = jest.fn();
render(<DataRetentionSelector value={defaultValue} onChange={onChange} />);

expect(screen.getByLabelText("Retention Time")).toHaveValue(30);
expect(screen.getByLabelText("Unit")).toHaveTextContent("Days");
});

test("calls onChange when value changes", () => {
const onChange = jest.fn();
render(<DataRetentionSelector value={defaultValue} onChange={onChange} />);

const valueInput = screen.getByLabelText("Retention Time");
userEvent.clear(valueInput);
userEvent.type(valueInput, "60");

expect(onChange).toHaveBeenCalledWith({
value: 60,
unit: TimeUnit.DAYS,
});
});

test("calls onChange when unit changes", () => {
const onChange = jest.fn();
render(<DataRetentionSelector value={defaultValue} onChange={onChange} />);

const unitSelect = screen.getByLabelText("Unit");
userEvent.click(unitSelect);
userEvent.click(screen.getByText("Weeks"));

expect(onChange).toHaveBeenCalledWith({
value: 30,
unit: TimeUnit.WEEKS,
});
});

test("displays all time unit options", () => {
const onChange = jest.fn();
render(<DataRetentionSelector value={defaultValue} onChange={onChange} />);

const unitSelect = screen.getByLabelText("Unit");
userEvent.click(unitSelect);

expect(screen.getAllByText("Days").length).toBeGreaterThan(0);
expect(screen.getByText("Weeks")).toBeInTheDocument();
expect(screen.getByText("Months")).toBeInTheDocument();
expect(screen.getByText("Years")).toBeInTheDocument();
});

test("can be disabled", () => {
const onChange = jest.fn();
render(
<DataRetentionSelector
value={defaultValue}
onChange={onChange}
disabled={true}
/>
);

const valueInput = screen.getByLabelText("Retention Time");
const unitSelect = screen.getByLabelText("Unit");

expect(valueInput).toBeDisabled();
expect(unitSelect).toHaveAttribute("aria-disabled", "true");
});

test("minimum allowed value is 1", () => {
const onChange = jest.fn();
render(<DataRetentionSelector value={defaultValue} onChange={onChange} />);

const valueInput = screen.getByLabelText("Retention Time");
expect(valueInput).toHaveAttribute("min", "1");
});
});
77 changes: 77 additions & 0 deletions src/components/DataRetentionSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from "react";
import { Stack, TextField, MenuItem } from "@mui/material";

export enum TimeUnit {
DAYS = "days",
WEEKS = "weeks",
MONTHS = "months",
YEARS = "years",
}

export type DataRetentionValue = {
value: number;
unit: TimeUnit;
};

type DataRetentionSelectorProps = {
value: DataRetentionValue;
onChange: (value: DataRetentionValue) => void;
disabled?: boolean;
};

const DataRetentionSelector: React.FC<DataRetentionSelectorProps> = ({
value,
onChange,
disabled = false,
}) => {
const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseInt(event.target.value, 10);
if (isNaN(newValue) && event.target.value !== "") {
return;
}
onChange({
...value,
value: isNaN(newValue) ? 1 : Math.max(1, newValue),
});
};

const handleUnitChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
unit: event.target.value as TimeUnit,
});
};

return (
<Stack direction="row" spacing={2} alignItems="center">
<TextField
id="retention-time"
label="Retention Time"
type="number"
value={value.value}
onChange={handleValueChange}
disabled={disabled}
inputProps={{
min: 1,
}}
sx={{ flex: 1 }}
/>
<TextField
id="retention-unit"
select
label="Unit"
value={value.unit}
onChange={handleUnitChange}
disabled={disabled}
sx={{ flex: 1 }}
>
<MenuItem value={TimeUnit.DAYS}>Days</MenuItem>
<MenuItem value={TimeUnit.WEEKS}>Weeks</MenuItem>
<MenuItem value={TimeUnit.MONTHS}>Months</MenuItem>
<MenuItem value={TimeUnit.YEARS}>Years</MenuItem>
</TextField>
</Stack>
);
};

export default DataRetentionSelector;
2 changes: 2 additions & 0 deletions src/scenes/AdminControl/AdminControl.js

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please place the component at the bottom instead of the top

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to bottom of AdminControl in commit a33b3e2. The component now appears after AdminAddLocation.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AdminAddVehicle from "./Components/AdminAddVehicle";
import AdminAddLocation from "./Components/AdminAddLocation";
import AdminAddDeliverableType from "./Components/AdminAddDeliverableType";
import AdminAddRiderResponsibility from "./Components/AdminAddRiderResponsibility";
import AdminDataRetention from "./Components/AdminDataRetention";
import { Stack } from "@mui/material";
import { RiderResponsibilityChips } from "./Components/RiderResponsibilityChips";
import { DeliverableTypeChips } from "./Components/DeliverableTypeChips";
Expand All @@ -19,6 +20,7 @@ export function AdminControl() {
<AdminAddUser />
<AdminAddVehicle />
<AdminAddLocation />
<AdminDataRetention />
</Stack>
);
}
157 changes: 157 additions & 0 deletions src/scenes/AdminControl/Components/AdminDataRetention.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from "react";
import AdminDataRetention from "./AdminDataRetention";
import { render } from "../../../test-utils";
import * as models from "../../../models";
import { userRoles } from "../../../apiConsts";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const preloadedState = {
whoami: {
user: new models.User({ roles: [userRoles.user, userRoles.admin] }),
},
loadingReducer: {
GET_WHOAMI: false,
},
tenantId: "tenant-id",
};

describe("AdminDataRetention", () => {
beforeEach(() => {
jest.restoreAllMocks();
});

test("renders the component with default values", async () => {
render(<AdminDataRetention />, { preloadedState });

expect(
screen.getByText("Set data retention time")
).toBeInTheDocument();
expect(
screen.getByText(
"Determine how long data should be retained before automatic deletion"
)
).toBeInTheDocument();
expect(
screen.getByLabelText(
"Retain data indefinitely (no automatic deletion)"
)
).toBeChecked();
expect(screen.queryByLabelText("Retention Time")).toBeNull();
});

test("allows toggling indefinite retention", () => {
render(<AdminDataRetention />, { preloadedState });

const checkbox = screen.getByLabelText(
"Retain data indefinitely (no automatic deletion)"
);
expect(checkbox).toBeChecked();

userEvent.click(checkbox);

expect(checkbox).not.toBeChecked();
expect(screen.getByLabelText("Retention Time")).toBeInTheDocument();
expect(screen.getByLabelText("Unit")).toBeInTheDocument();
});

test("allows changing retention time value", () => {
render(<AdminDataRetention />, { preloadedState });

// Uncheck indefinite retention first
const checkbox = screen.getByLabelText(
"Retain data indefinitely (no automatic deletion)"
);
userEvent.click(checkbox);

const valueInput = screen.getByLabelText("Retention Time");
userEvent.clear(valueInput);
userEvent.type(valueInput, "60");

expect(valueInput).toHaveValue(60);
});

test("allows changing time unit", () => {
render(<AdminDataRetention />, { preloadedState });

// Uncheck indefinite retention first
const checkbox = screen.getByLabelText(
"Retain data indefinitely (no automatic deletion)"
);
userEvent.click(checkbox);

const unitSelect = screen.getByLabelText("Unit");
userEvent.click(unitSelect);
userEvent.click(screen.getByText("Weeks"));

// Check that the unit field now displays "Weeks"
expect(unitSelect).toHaveTextContent("Weeks");
});

test("save button is enabled and clickable", () => {
render(<AdminDataRetention />, { preloadedState });

const saveButton = screen.getByRole("button", {
name: "Save retention settings",
});
expect(saveButton).not.toBeDisabled();

userEvent.click(saveButton);
// Button should be clickable without errors
});

test("shows warning dialog for short retention periods", async () => {
render(<AdminDataRetention />, { preloadedState });

// Uncheck indefinite retention
const checkbox = screen.getByLabelText(
"Retain data indefinitely (no automatic deletion)"
);
userEvent.click(checkbox);

// Set a short retention period (5 days)
const valueInput = screen.getByLabelText("Retention Time");
userEvent.clear(valueInput);
userEvent.type(valueInput, "5");

// Click save
const saveButton = screen.getByRole("button", {
name: "Save retention settings",
});
userEvent.click(saveButton);

// Warning dialog should appear
await waitFor(() => {
expect(
screen.getByText("Short Retention Period Warning")
).toBeInTheDocument();
});
});

test("displays forbidden message if user is not admin", () => {
render(<AdminDataRetention />, {
preloadedState: {
...preloadedState,
whoami: { user: new models.User({ roles: [userRoles.user] }) },
},
});

expect(screen.queryByText("Set data retention time")).toBeNull();
expect(
screen.getByText("You don't have permission to view this page.")
).toBeInTheDocument();
});

test("displays skeleton while loading", () => {
render(<AdminDataRetention />, {
preloadedState: {
...preloadedState,
loadingReducer: {
GET_WHOAMI: true,
},
},
});

expect(screen.queryByText("Set data retention time")).toBeNull();
});
});
Loading