Remove duplicate node logic, UI Update, flatten build output (#586)
* refactor nodes to getNodes fn. ui updates * fixed several styling issues * fix: message specific styling/overflow * added footer, fixed tests. styling * fix: added theme support back to app component * fix: hide emojis/reactions * fix: added more padding to content element * fix: fixed padding in content element * updated color scheme * fix: more dark mode styling improvements * fix: padding and alignment fixes * fix: prevent left sidebar collapse, added battery component * fix: change scrollbars to "tiny" style, improved message scrolling, fixed bug with message input * message store fixes, ui fixes * fix: disabled message persistance until after release
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
// Ensure the path is correct for import
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||
|
||||
vi.mock('@core/stores/messageStore.ts', () => ({
|
||||
vi.mock('@core/stores/messageStore', () => ({
|
||||
useMessageStore: vi.fn(() => ({
|
||||
deleteAllMessages: vi.fn(),
|
||||
})),
|
||||
@@ -14,17 +15,36 @@ describe('DeleteMessagesDialog', () => {
|
||||
const mockClearAllMessages = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useMessageStore).mockReturnValue({ deleteAllMessages: mockClearAllMessages });
|
||||
mockOnOpenChange.mockClear();
|
||||
mockClearAllMessages.mockClear();
|
||||
|
||||
const mockedUseMessageStore = vi.mocked(useMessageStore);
|
||||
mockedUseMessageStore.mockImplementation(() => ({
|
||||
deleteAllMessages: mockClearAllMessages
|
||||
}));
|
||||
mockedUseMessageStore.mockClear();
|
||||
|
||||
});
|
||||
|
||||
it('calls onOpenChange with false when the close button (X) is clicked', () => {
|
||||
render(<DeleteMessagesDialog open={true} onOpenChange={mockOnOpenChange} />);
|
||||
const closeButton = screen.queryByTestId('dialog-close-button');
|
||||
if (!closeButton) {
|
||||
throw new Error("Dialog close button with data-testid='dialog-close-button' not found. Did you add it to the component?");
|
||||
}
|
||||
fireEvent.click(closeButton);
|
||||
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('renders the dialog when open is true', () => {
|
||||
render(<DeleteMessagesDialog open={true} onOpenChange={mockOnOpenChange} />);
|
||||
expect(screen.getByText('Clear All Messages')).toBeVisible();
|
||||
expect(screen.getByText(/This action will clear all message history./)).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: 'Clear Messages' })).toBeVisible();
|
||||
expect(screen.getByText('Clear All Messages')).toBeInTheDocument();
|
||||
expect(screen.getByText(/This action will clear all message history./)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Clear Messages' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the dialog when open is false', () => {
|
||||
@@ -32,15 +52,10 @@ describe('DeleteMessagesDialog', () => {
|
||||
expect(screen.queryByText('Clear All Messages')).toBeNull();
|
||||
});
|
||||
|
||||
it('calls onOpenChange with false when the close button is clicked', () => {
|
||||
render(<DeleteMessagesDialog open={true} onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('calls onOpenChange with false when the dismiss button is clicked', () => {
|
||||
render(<DeleteMessagesDialog open={true} onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
|
||||
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
@@ -48,6 +63,7 @@ describe('DeleteMessagesDialog', () => {
|
||||
render(<DeleteMessagesDialog open={true} onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Clear Messages' }));
|
||||
expect(mockClearAllMessages).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
|
||||
export interface DeleteMessagesDialogProps {
|
||||
open: boolean;
|
||||
@@ -29,7 +29,7 @@ export const DeleteMessagesDialog = ({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogClose onClick={handleCloseDialog} />
|
||||
<DialogClose data-testid="dialog-close-button" />
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangleIcon className="h-5 w-5 text-warning" />
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { GenericInput } from "@components/Form/FormInput.tsx";
|
||||
import { validateMaxByteLength } from "@core/utils/string.ts";
|
||||
|
||||
export interface User {
|
||||
longName: string;
|
||||
@@ -24,32 +25,44 @@ export interface DeviceNameDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
const MAX_LONG_NAME_BYTE_LENGTH = 40;
|
||||
const MAX_SHORT_NAME_BYTE_LENGTH = 4;
|
||||
|
||||
export const DeviceNameDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DeviceNameDialogProps) => {
|
||||
const { hardware, nodes, connection } = useDevice();
|
||||
const { hardware, getNode, connection } = useDevice();
|
||||
const myNode = getNode(hardware.myNodeNum);
|
||||
|
||||
const myNode = nodes.get(hardware.myNodeNum);
|
||||
const defaultValues = {
|
||||
longName: myNode?.user?.longName ?? "Unknown",
|
||||
shortName: myNode?.user?.shortName ?? "??",
|
||||
};
|
||||
|
||||
const { register, handleSubmit } = useForm<User>({
|
||||
values: {
|
||||
longName: myNode?.user?.longName ?? "Unknown",
|
||||
shortName: myNode?.user?.shortName ?? "Unknown",
|
||||
},
|
||||
const { getValues, setValue, reset, control, handleSubmit } = useForm<User>({
|
||||
values: defaultValues,
|
||||
});
|
||||
|
||||
const { currentLength: currentLongNameLength } = validateMaxByteLength(getValues('longName'), MAX_LONG_NAME_BYTE_LENGTH);
|
||||
const { currentLength: currentShortNameLength } = validateMaxByteLength(getValues('shortName'), MAX_SHORT_NAME_BYTE_LENGTH);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
connection?.setOwner(
|
||||
create(Protobuf.Mesh.UserSchema, {
|
||||
...myNode?.user,
|
||||
...(myNode?.user ?? {}),
|
||||
...data,
|
||||
}),
|
||||
);
|
||||
onOpenChange(false);
|
||||
});
|
||||
|
||||
const handleReset = () => {
|
||||
reset({ longName: "", shortName: "" });
|
||||
setValue("longName", "");
|
||||
setValue("shortName", "");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
@@ -60,22 +73,51 @@ export const DeviceNameDialog = ({
|
||||
The Device will restart once the config is saved.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="gap-4">
|
||||
<form onSubmit={onSubmit}>
|
||||
<Label>Long Name</Label>
|
||||
<Input className="dark:text-slte-900" {...register("longName")} />
|
||||
<Label>Short Name</Label>
|
||||
<Input
|
||||
className="dark:text-slte-900"
|
||||
maxLength={4}
|
||||
{...register("shortName")}
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label htmlFor="longName">Long Name</Label>
|
||||
<GenericInput
|
||||
control={control}
|
||||
field={{
|
||||
name: "longName",
|
||||
label: "Long Name",
|
||||
type: "text",
|
||||
properties: {
|
||||
className: 'text-slate-900 dark:text-slate-200',
|
||||
fieldLength: {
|
||||
currentValueLength: currentLongNameLength ?? 0,
|
||||
max: MAX_LONG_NAME_BYTE_LENGTH,
|
||||
showCharacterCount: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => onSubmit()}>Save</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="shortName">Short Name</Label>
|
||||
<GenericInput
|
||||
control={control}
|
||||
field={{
|
||||
name: "shortName",
|
||||
label: "Short Name",
|
||||
type: "text",
|
||||
properties: {
|
||||
fieldLength: {
|
||||
currentValueLength: currentShortNameLength ?? 0,
|
||||
max: MAX_SHORT_NAME_BYTE_LENGTH,
|
||||
showCharacterCount: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="destructive" onClick={handleReset}>Reset</Button>
|
||||
<Button type="submit">Save</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -109,7 +109,6 @@ export const ImportDialog = ({
|
||||
<Input
|
||||
value={importDialogInput}
|
||||
suffix={validUrl ? "✅" : "❌"}
|
||||
className="dark:text-slate-900"
|
||||
onChange={(e) => {
|
||||
setImportDialogInput(e.target.value);
|
||||
}}
|
||||
|
||||
@@ -21,9 +21,9 @@ export const LocationResponseDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: LocationResponseDialogProps) => {
|
||||
const { nodes } = useDevice();
|
||||
const { getNode } = useDevice();
|
||||
|
||||
const from = nodes.get(location?.from ?? 0);
|
||||
const from = getNode(location?.from ?? 0);
|
||||
const longName = from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||
const shortName = from?.user?.shortName ??
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { describe, it, vi, expect, beforeEach, Mock } from "vitest";
|
||||
import { describe, it, vi, expect, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
vi.mock("@core/stores/deviceStore");
|
||||
vi.mock("@core/stores/appStore");
|
||||
|
||||
const mockUseDevice = vi.mocked(useDevice);
|
||||
const mockUseAppStore = vi.mocked(useAppStore);
|
||||
|
||||
|
||||
describe("NodeDetailsDialog", () => {
|
||||
const mockDevice = {
|
||||
num: 1234,
|
||||
@@ -29,17 +34,21 @@ describe("NodeDetailsDialog", () => {
|
||||
voltage: 4.2,
|
||||
uptimeSeconds: 3600,
|
||||
},
|
||||
};
|
||||
} as unknown as Protobuf.Mesh.NodeInfo;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
vi.resetAllMocks();
|
||||
|
||||
(useDevice as Mock).mockReturnValue({
|
||||
nodes: new Map([[1234, mockDevice]]),
|
||||
mockUseDevice.mockReturnValue({
|
||||
getNode: (nodeNum: number) => {
|
||||
if (nodeNum === 1234) {
|
||||
return mockDevice;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
(useAppStore as unknown as Mock).mockReturnValue({
|
||||
mockUseAppStore.mockReturnValue({
|
||||
nodeNumDetails: 1234,
|
||||
});
|
||||
});
|
||||
@@ -47,27 +56,87 @@ describe("NodeDetailsDialog", () => {
|
||||
it("renders node details correctly", () => {
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
|
||||
expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Node Details for Test Node \(TN\)/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Node Number: 1234")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Node Hex: !/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Last Heard:/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Coordinates:/i)).toBeInTheDocument();
|
||||
const link = screen.getByRole('link', { name: /^45, -75$/ });
|
||||
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', expect.stringContaining('openstreetmap.org'));
|
||||
expect(screen.getByText(/Altitude: 200m/i)).toBeInTheDocument();
|
||||
|
||||
|
||||
expect(screen.getByText(/Air TX utilization: 50.12%/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Channel utilization: 75.46%/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Battery level: 88.79%/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Voltage: 4.20V/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Uptime:/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Coordinates:/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("45, -75")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Altitude: 200m/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Role:/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/All Raw Metrics:/i)).toBeInTheDocument();
|
||||
|
||||
});
|
||||
|
||||
it("renders null if device is not found", () => {
|
||||
(useDevice as Mock).mockReturnValue({
|
||||
nodes: new Map(),
|
||||
const requestedNodeNum = 5678;
|
||||
|
||||
mockUseAppStore.mockReturnValue({
|
||||
nodeNumDetails: requestedNodeNum,
|
||||
});
|
||||
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
mockUseDevice.mockReturnValue({
|
||||
getNode: (nodeNum: number) => {
|
||||
if (nodeNum === requestedNodeNum) {
|
||||
return undefined;
|
||||
}
|
||||
if (nodeNum === 1234) {
|
||||
return mockDevice;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.queryByText(/Node Details for/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders correctly when position is missing", () => {
|
||||
const nodeWithoutPosition = { ...mockDevice, position: undefined };
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutPosition });
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 });
|
||||
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
|
||||
expect(screen.queryByText(/Coordinates:/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Altitude:/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders correctly when deviceMetrics are missing", () => {
|
||||
const nodeWithoutMetrics = { ...mockDevice, deviceMetrics: undefined };
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutMetrics });
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 });
|
||||
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
|
||||
expect(screen.queryByText(/Device Metrics:/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Air TX utilization:/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders 'Never' for lastHeard when timestamp is 0", () => {
|
||||
const nodeNeverHeard = { ...mockDevice, lastHeard: 0 };
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeNeverHeard });
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 });
|
||||
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
|
||||
expect(screen.getByText(/Last Heard: Never/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -29,10 +29,10 @@ export const NodeDetailsDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NodeDetailsDialogProps) => {
|
||||
const { nodes } = useDevice();
|
||||
const { getNode } = useDevice();
|
||||
const { nodeNumDetails } = useAppStore();
|
||||
|
||||
const device = nodes.get(nodeNumDetails);
|
||||
const device = getNode(nodeNumDetails);
|
||||
|
||||
if (!device) return null;
|
||||
|
||||
@@ -131,7 +131,7 @@ export const NodeDetailsDialog = ({
|
||||
|
||||
{device.deviceMetrics && (
|
||||
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-0">
|
||||
Device Metrics:
|
||||
</p>
|
||||
{deviceMetricsMap.map(
|
||||
|
||||
@@ -13,7 +13,7 @@ import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "../UI/Button.tsx";
|
||||
import { MessageType, useMessageStore } from "@core/stores/messageStore.ts";
|
||||
import { MessageType, useMessageStore } from "../../core/stores/messageStore/index.ts";
|
||||
|
||||
export interface NodeOptionsDialogProps {
|
||||
node: Protobuf.Mesh.NodeInfo | undefined;
|
||||
|
||||
@@ -133,8 +133,8 @@ export const QRDialog = ({
|
||||
<Input
|
||||
value={qrCodeUrl}
|
||||
disabled
|
||||
className="dark:text-slate-900"
|
||||
action={{
|
||||
key: 'copy-value',
|
||||
icon: ClipboardIcon,
|
||||
onClick() {
|
||||
void navigator.clipboard.writeText(qrCodeUrl);
|
||||
|
||||
@@ -1,102 +1,97 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi, Mock } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { DeviceContext, useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts"; // Import for mocking
|
||||
import { useDevice } from "@core/stores/deviceStore.ts"; // Import for mocking
|
||||
import { expect, test, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
vi.mock("@core/stores/messageStore");
|
||||
vi.mock("./useRefreshKeysDialog");
|
||||
|
||||
vi.mock("@core/stores/messageStore.ts", () => ({
|
||||
useMessageStore: vi.fn(),
|
||||
}));
|
||||
const mockUseMessageStore = vi.mocked(useMessageStore);
|
||||
const mockUseRefreshKeysDialog = vi.mocked(useRefreshKeysDialog);
|
||||
|
||||
const mockNodeWithError: Partial<Protobuf.Mesh.NodeInfo> = {
|
||||
user: { longName: "Test Node Long", shortName: "TNL", id: 456 },
|
||||
};
|
||||
const mockNodes = new Map([[456, mockNodeWithError]]);
|
||||
const mockNodeErrors = new Map([[123, { node: 456 }]]);
|
||||
const getInitialState = () => useDeviceStore.getInitialState?.() ?? { devices: new Map(), remoteDevices: new Map() };
|
||||
|
||||
vi.mock("@core/stores/deviceStore.ts", () => ({
|
||||
useDevice: vi.fn(),
|
||||
}));
|
||||
beforeEach(() => {
|
||||
useDeviceStore.setState(getInitialState(), true);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockHandleCloseDialog = vi.fn();
|
||||
const mockHandleNodeRemove = vi.fn();
|
||||
vi.mock("./useRefreshKeysDialog.ts", () => ({
|
||||
useRefreshKeysDialog: vi.fn(() => ({
|
||||
handleCloseDialog: mockHandleCloseDialog,
|
||||
handleNodeRemove: mockHandleNodeRemove,
|
||||
})),
|
||||
}));
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("RefreshKeysDialog Component", () => {
|
||||
let onOpenChangeMock: Mock;
|
||||
test("renders dialog when there is a node error for the active chat", () => {
|
||||
const deviceId = 1;
|
||||
const nodeWithErrorNum = 12345;
|
||||
const activeChatNum = nodeWithErrorNum;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
onOpenChangeMock = vi.fn();
|
||||
const deviceStore = useDeviceStore.getState().addDevice(deviceId);
|
||||
|
||||
vi.mocked(useMessageStore).mockReturnValue({ activeChat: 123 });
|
||||
vi.mocked(useDevice).mockReturnValue({
|
||||
nodeErrors: mockNodeErrors,
|
||||
nodes: mockNodes,
|
||||
});
|
||||
vi.mocked(useRefreshKeysDialog).mockReturnValue({
|
||||
handleCloseDialog: mockHandleCloseDialog,
|
||||
handleNodeRemove: mockHandleNodeRemove,
|
||||
});
|
||||
deviceStore.addNodeInfo({
|
||||
num: nodeWithErrorNum,
|
||||
user: {
|
||||
id: nodeWithErrorNum.toString(),
|
||||
publicKey: new Uint8Array(0),
|
||||
hwModel: Protobuf.Mesh.HardwareModel.HELTEC_V3,
|
||||
longName: "Problem Node Long",
|
||||
shortName: "ProbNode",
|
||||
isLicensed: false,
|
||||
macaddr: new Uint8Array(0)
|
||||
},
|
||||
lastHeard: Date.now() / 1000,
|
||||
snr: 10
|
||||
} as Protobuf.Mesh.NodeInfo);
|
||||
|
||||
deviceStore.setNodeError(activeChatNum, "PKI_MISMATCH");
|
||||
|
||||
const updatedDeviceState = useDeviceStore.getState().getDevice(deviceId);
|
||||
if (!updatedDeviceState) {
|
||||
throw new Error("Failed to get updated device state from store for provider");
|
||||
}
|
||||
|
||||
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
|
||||
const mockHandleClose = vi.fn();
|
||||
const mockHandleRemove = vi.fn();
|
||||
mockUseRefreshKeysDialog.mockReturnValue({
|
||||
handleCloseDialog: mockHandleClose,
|
||||
handleNodeRemove: mockHandleRemove,
|
||||
});
|
||||
|
||||
it("should render the dialog with dynamic content when open and data is available", () => {
|
||||
render(<RefreshKeysDialog open onOpenChange={onOpenChangeMock} />);
|
||||
render(
|
||||
<DeviceContext.Provider value={updatedDeviceState}>
|
||||
<RefreshKeysDialog open onOpenChange={vi.fn()} />
|
||||
</DeviceContext.Provider>
|
||||
);
|
||||
|
||||
expect(screen.getByText(`Keys Mismatch - ${mockNodeWithError?.user?.longName}`)).toBeInTheDocument();
|
||||
expect(screen.getByText(new RegExp(`${mockNodeWithError?.user?.longName}.*${mockNodeWithError?.user?.shortName}`))).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /request new keys/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /dismiss/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Close/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Keys Mismatch - Problem Node Long/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Your node is unable to send a direct message to node: Problem Node Long \(ProbNode\)/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Request New Keys" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render dialog if no error exists for active chat", () => {
|
||||
const deviceId = 1;
|
||||
const activeChatNum = 54321;
|
||||
|
||||
useDeviceStore.getState().addDevice(deviceId);
|
||||
|
||||
const currentDeviceState = useDeviceStore.getState().getDevice(deviceId);
|
||||
if (!currentDeviceState) throw new Error("Device not found");
|
||||
|
||||
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
|
||||
mockUseRefreshKeysDialog.mockReturnValue({
|
||||
handleCloseDialog: vi.fn(),
|
||||
handleNodeRemove: vi.fn(),
|
||||
});
|
||||
|
||||
it("should call handleNodeRemove when 'Request New Keys' button is clicked", () => {
|
||||
render(<RefreshKeysDialog open onOpenChange={onOpenChangeMock} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /request new keys/i }));
|
||||
expect(mockHandleNodeRemove).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const { container } = render(
|
||||
<DeviceContext.Provider value={currentDeviceState}>
|
||||
<RefreshKeysDialog open onOpenChange={vi.fn()} />
|
||||
</DeviceContext.Provider>
|
||||
);
|
||||
|
||||
it("should call handleCloseDialog when 'Dismiss' button is clicked", () => {
|
||||
render(<RefreshKeysDialog open onOpenChange={onOpenChangeMock} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /dismiss/i }));
|
||||
expect(mockHandleCloseDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call handleCloseDialog when the explicit DialogClose button is clicked", () => {
|
||||
render(<RefreshKeysDialog open onOpenChange={onOpenChangeMock} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /close/i })); // Use the aria-label
|
||||
expect(mockHandleCloseDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
it("should not render the dialog when open is false", () => {
|
||||
render(<RefreshKeysDialog open={false} onOpenChange={onOpenChangeMock} />);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render null if nodeErrorNum is not found for activeChat", () => {
|
||||
vi.mocked(useDevice).mockReturnValue({
|
||||
nodeErrors: new Map(),
|
||||
nodes: mockNodes,
|
||||
});
|
||||
const { container } = render(<RefreshKeysDialog open onOpenChange={onOpenChangeMock} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should render null if nodeWithError is not found for nodeErrorNum.node", () => {
|
||||
vi.mocked(useDevice).mockReturnValue({
|
||||
nodeErrors: mockNodeErrors,
|
||||
nodes: new Map(),
|
||||
});
|
||||
const { container } = render(<RefreshKeysDialog open onOpenChange={onOpenChangeMock} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Button } from "@components/UI/Button.tsx";
|
||||
import { LockKeyholeOpenIcon } from "lucide-react";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
||||
|
||||
export interface RefreshKeysDialogProps {
|
||||
open: boolean;
|
||||
@@ -18,22 +18,16 @@ export interface RefreshKeysDialogProps {
|
||||
|
||||
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
|
||||
const { activeChat } = useMessageStore();
|
||||
const { nodeErrors, nodes } = useDevice();
|
||||
const { nodeErrors, getNode } = useDevice();
|
||||
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
|
||||
|
||||
const nodeErrorNum = nodeErrors.get(activeChat);
|
||||
|
||||
if (!nodeErrorNum) {
|
||||
console.error("Node with error not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeWithError = nodes.get(nodeErrorNum?.node ?? 0);
|
||||
|
||||
if (!nodeWithError) {
|
||||
console.error("Node with error not found");
|
||||
return null;
|
||||
}
|
||||
const nodeWithError = getNode(nodeErrorNum.node);
|
||||
|
||||
const text = {
|
||||
title: `Keys Mismatch - ${nodeWithError?.user?.longName ?? ""}`,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { renderHook, act } from "@testing-library/react";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
|
||||
vi.mock("@core/stores/messageStore.ts", () => ({
|
||||
vi.mock("@core/stores/messageStore", () => ({
|
||||
useMessageStore: vi.fn(() => ({ activeChat: "chat-123" })),
|
||||
}));
|
||||
|
||||
vi.mock("@core/stores/deviceStore.ts", () => ({
|
||||
vi.mock("@core/stores/deviceStore", () => ({
|
||||
useDevice: vi.fn(() => ({
|
||||
removeNode: vi.fn(),
|
||||
setDialogOpen: vi.fn(),
|
||||
@@ -23,46 +23,50 @@ describe("useRefreshKeysDialog Hook", () => {
|
||||
let clearNodeErrorMock: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
removeNodeMock = vi.fn();
|
||||
setDialogOpenMock = vi.fn();
|
||||
getNodeErrorMock = vi.fn();
|
||||
getNodeErrorMock = vi.fn().mockReturnValue(undefined);
|
||||
clearNodeErrorMock = vi.fn();
|
||||
|
||||
(useDevice as Mock).mockReturnValue({
|
||||
vi.mocked(useDevice).mockReturnValue({
|
||||
removeNode: removeNodeMock,
|
||||
setDialogOpen: setDialogOpenMock,
|
||||
getNodeError: getNodeErrorMock,
|
||||
clearNodeError: clearNodeErrorMock,
|
||||
});
|
||||
|
||||
vi.mocked(useMessageStore).mockReturnValue({
|
||||
activeChat: "chat-123"
|
||||
});
|
||||
});
|
||||
|
||||
it("handleNodeRemove should remove the node and update dialog if there is an error", () => {
|
||||
getNodeErrorMock.mockReturnValue({ node: "node-abc" });
|
||||
|
||||
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||
act(() => { result.current.handleNodeRemove(); });
|
||||
|
||||
act(() => {
|
||||
result.current.handleNodeRemove();
|
||||
});
|
||||
|
||||
expect(getNodeErrorMock).toHaveBeenCalledTimes(1);
|
||||
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||
expect(clearNodeErrorMock).toHaveBeenCalledTimes(1);
|
||||
expect(clearNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||
expect(removeNodeMock).toHaveBeenCalledTimes(1);
|
||||
expect(removeNodeMock).toHaveBeenCalledWith("node-abc");
|
||||
expect(setDialogOpenMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
|
||||
});
|
||||
|
||||
it("handleNodeRemove should do nothing if there is no error", () => {
|
||||
getNodeErrorMock.mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||
act(() => { result.current.handleNodeRemove(); });
|
||||
|
||||
act(() => {
|
||||
result.current.handleNodeRemove();
|
||||
});
|
||||
|
||||
expect(getNodeErrorMock).toHaveBeenCalledTimes(1);
|
||||
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||
expect(clearNodeErrorMock).not.toHaveBeenCalled();
|
||||
expect(removeNodeMock).not.toHaveBeenCalled();
|
||||
expect(setDialogOpenMock).not.toHaveBeenCalled();
|
||||
expect(clearNodeErrorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handleCloseDialog should close the dialog", () => {
|
||||
@@ -72,6 +76,7 @@ describe("useRefreshKeysDialog Hook", () => {
|
||||
result.current.handleCloseDialog();
|
||||
});
|
||||
|
||||
expect(setDialogOpenMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,28 +1,27 @@
|
||||
import { useCallback } from "react";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
|
||||
export function useRefreshKeysDialog() {
|
||||
const { removeNode, setDialogOpen, clearNodeError, getNodeError } = useDevice();
|
||||
const { activeChat } = useMessageStore();
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setDialogOpen('refreshKeys', false);
|
||||
}, [setDialogOpen]);
|
||||
|
||||
const handleNodeRemove = useCallback(() => {
|
||||
const nodeWithError = getNodeError(activeChat);
|
||||
if (!nodeWithError) {
|
||||
return;
|
||||
}
|
||||
clearNodeError(activeChat);
|
||||
handleCloseDialog();;
|
||||
handleCloseDialog();
|
||||
return removeNode(nodeWithError?.node);
|
||||
}, [activeChat, clearNodeError, setDialogOpen, removeNode]);
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setDialogOpen('refreshKeys', false);
|
||||
}, [setDialogOpen])
|
||||
}, [activeChat, clearNodeError, getNodeError, removeNode, handleCloseDialog]);
|
||||
|
||||
return {
|
||||
handleCloseDialog,
|
||||
handleNodeRemove
|
||||
};
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const RemoveNodeDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RemoveNodeDialogProps) => {
|
||||
const { connection, nodes, removeNode } = useDevice();
|
||||
const { connection, getNode, removeNode } = useDevice();
|
||||
const { nodeNumToBeRemoved } = useAppStore();
|
||||
|
||||
const onSubmit = () => {
|
||||
@@ -42,7 +42,7 @@ export const RemoveNodeDialog = ({
|
||||
</DialogHeader>
|
||||
<div className="gap-4">
|
||||
<form onSubmit={onSubmit}>
|
||||
<Label>{nodes.get(nodeNumToBeRemoved)?.user?.longName}</Label>
|
||||
<Label>{getNode(nodeNumToBeRemoved)?.user?.longName}</Label>
|
||||
</form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -41,7 +41,6 @@ export const ShutdownDialog = ({
|
||||
type="number"
|
||||
value={time}
|
||||
onChange={(e) => setTime(Number.parseInt(e.target.value))}
|
||||
className="dark:text-slate-900"
|
||||
suffix="Minutes"
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -23,17 +23,17 @@ export const TracerouteResponseDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: TracerouteResponseDialogProps) => {
|
||||
const { nodes } = useDevice();
|
||||
const { getNode } = useDevice();
|
||||
const route: number[] = traceroute?.data.route ?? [];
|
||||
const routeBack: number[] = traceroute?.data.routeBack ?? [];
|
||||
const snrTowards = (traceroute?.data.snrTowards ?? []).map(snr => snr / 4);
|
||||
const snrBack = (traceroute?.data.snrBack ?? []).map(snr => snr / 4);
|
||||
const from = nodes.get(traceroute?.from ?? 0);
|
||||
const from = getNode(traceroute?.from ?? 0);
|
||||
const longName = from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||
const shortName = from?.user?.shortName ??
|
||||
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
|
||||
const to = nodes.get(traceroute?.to ?? 0);
|
||||
const to = getNode(traceroute?.to ?? 0);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user