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:
Dan Ditomaso
2025-05-02 09:00:24 -04:00
committed by GitHub
parent 3eafad7261
commit 99711fc44e
78 changed files with 2764 additions and 2028 deletions
@@ -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" />
+66 -24
View File
@@ -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>
);
};
};
-1
View File
@@ -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(
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
};
}
+2 -2
View File
@@ -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>
-1
View File
@@ -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>