chore: lint/format all files (#604)
* chore: lint/format all files * Fix config sidebar button state (#602) * chore: Update deno.lock version and add Radix UI slider component (#601) * fix: improve how table addresses even/odd rows --------- Co-authored-by: philon- <philon-@users.noreply.github.com> Co-authored-by: Kamil Dzieniszewski <kamil.dzieniszewski@gmail.com>
This commit is contained in:
co-authored by
philon-
Kamil Dzieniszewski
parent
b7bdb1a502
commit
480ca46a95
@@ -1,16 +1,16 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
// 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', () => ({
|
||||
vi.mock("@core/stores/messageStore", () => ({
|
||||
useMessageStore: vi.fn(() => ({
|
||||
deleteAllMessages: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('DeleteMessagesDialog', () => {
|
||||
describe("DeleteMessagesDialog", () => {
|
||||
const mockOnOpenChange = vi.fn();
|
||||
const mockClearAllMessages = vi.fn();
|
||||
|
||||
@@ -20,50 +20,61 @@ describe('DeleteMessagesDialog', () => {
|
||||
|
||||
const mockedUseMessageStore = vi.mocked(useMessageStore);
|
||||
mockedUseMessageStore.mockImplementation(() => ({
|
||||
deleteAllMessages: mockClearAllMessages
|
||||
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');
|
||||
it("calls onOpenChange with false when the close button (X) is clicked", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open 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?");
|
||||
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')).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("renders the dialog when open is true", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
|
||||
);
|
||||
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', () => {
|
||||
render(<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />);
|
||||
expect(screen.queryByText('Clear All Messages')).toBeNull();
|
||||
it("does not render the dialog when open is false", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />,
|
||||
);
|
||||
expect(screen.queryByText("Clear All Messages")).toBeNull();
|
||||
});
|
||||
|
||||
it('calls onOpenChange with false when the dismiss button is clicked', () => {
|
||||
render(<DeleteMessagesDialog open={true} onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
|
||||
it("calls onOpenChange with false when the dismiss button is clicked", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
|
||||
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('calls deleteAllMessages and onOpenChange with false when the clear messages button is clicked', () => {
|
||||
render(<DeleteMessagesDialog open={true} onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Clear Messages' }));
|
||||
it("calls deleteAllMessages and onOpenChange with false when the clear messages button is clicked", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
|
||||
@@ -44,8 +44,14 @@ export const DeviceNameDialog = ({
|
||||
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 { 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(
|
||||
@@ -83,7 +89,7 @@ export const DeviceNameDialog = ({
|
||||
label: "Long Name",
|
||||
type: "text",
|
||||
properties: {
|
||||
className: 'text-slate-900 dark:text-slate-200',
|
||||
className: "text-slate-900 dark:text-slate-200",
|
||||
fieldLength: {
|
||||
currentValueLength: currentLongNameLength ?? 0,
|
||||
max: MAX_LONG_NAME_BYTE_LENGTH,
|
||||
@@ -113,11 +119,13 @@ export const DeviceNameDialog = ({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="destructive" onClick={handleReset}>Reset</Button>
|
||||
<Button type="button" variant="destructive" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit">Save</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshK
|
||||
import { RebootOTADialog } from "@components/Dialog/RebootOTADialog.tsx";
|
||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||
|
||||
|
||||
export const DialogManager = () => {
|
||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||
return (
|
||||
|
||||
@@ -51,7 +51,7 @@ export const ImportDialog = ({
|
||||
const paddedString = encodedChannelConfig
|
||||
.padEnd(
|
||||
encodedChannelConfig.length +
|
||||
((4 - (encodedChannelConfig.length % 4)) % 4),
|
||||
((4 - (encodedChannelConfig.length % 4)) % 4),
|
||||
"=",
|
||||
)
|
||||
.replace(/-/g, "+")
|
||||
|
||||
@@ -43,8 +43,9 @@ export const LocationResponseDialog = ({
|
||||
Coordinates:{" "}
|
||||
<a
|
||||
className="text-blue-500 dark:text-blue-400"
|
||||
href={`https://www.openstreetmap.org/?mlat=${location?.data.latitudeI / 1e7
|
||||
}&mlon=${location?.data.longitudeI / 1e7}&layers=N`}
|
||||
href={`https://www.openstreetmap.org/?mlat=${
|
||||
location?.data.latitudeI / 1e7
|
||||
}&mlon=${location?.data.longitudeI / 1e7}&layers=N`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -90,8 +90,8 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
||||
{browserFeatures.length > 0 && (
|
||||
<>
|
||||
This connection type requires{" "}
|
||||
{formatFeatureList(browserFeatures)}. Please use a
|
||||
supported browser, like Chrome or Edge.
|
||||
{formatFeatureList(browserFeatures)}. Please use a supported
|
||||
browser, like Chrome or Edge.
|
||||
</>
|
||||
)}
|
||||
{needsSecureContext && (
|
||||
@@ -114,11 +114,9 @@ export const NewDeviceDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NewDeviceProps) => {
|
||||
const [connectionInProgress, setConnectionInProgress] =
|
||||
useState(false);
|
||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||
const { unsupported } = useBrowserFeatureDetection();
|
||||
|
||||
|
||||
const tabs: TabManifest[] = [
|
||||
{
|
||||
label: "HTTP",
|
||||
@@ -160,7 +158,11 @@ export const NewDeviceDialog = ({
|
||||
{tab.isDisabled
|
||||
? <ErrorMessage missingFeatures={unsupported} />
|
||||
: null}
|
||||
<tab.element closeDialog={() => onOpenChange(false)} setConnectionInProgress={setConnectionInProgress} connectionInProgress={connectionInProgress} />
|
||||
<tab.element
|
||||
closeDialog={() => onOpenChange(false)}
|
||||
setConnectionInProgress={setConnectionInProgress}
|
||||
connectionInProgress={connectionInProgress}
|
||||
/>
|
||||
</fieldset>
|
||||
</TabsContent>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, vi, expect, beforeEach } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -11,7 +11,6 @@ vi.mock("@core/stores/appStore");
|
||||
const mockUseDevice = vi.mocked(useDevice);
|
||||
const mockUseAppStore = vi.mocked(useAppStore);
|
||||
|
||||
|
||||
describe("NodeDetailsDialog", () => {
|
||||
const mockDevice = {
|
||||
num: 1234,
|
||||
@@ -54,30 +53,33 @@ describe("NodeDetailsDialog", () => {
|
||||
});
|
||||
|
||||
it("renders node details correctly", () => {
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />);
|
||||
|
||||
expect(screen.getByText(/Node Details for Test Node \(TN\)/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$/ });
|
||||
const link = screen.getByRole("link", { name: /^45, -75$/ });
|
||||
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', expect.stringContaining('openstreetmap.org'));
|
||||
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(/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(/All Raw Metrics:/i)).toBeInTheDocument();
|
||||
|
||||
});
|
||||
|
||||
it("renders null if device is not found", () => {
|
||||
@@ -99,7 +101,9 @@ describe("NodeDetailsDialog", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
const { container } = render(
|
||||
<NodeDetailsDialog open onOpenChange={() => {}} />,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.queryByText(/Node Details for/i)).not.toBeInTheDocument();
|
||||
@@ -110,7 +114,7 @@ describe("NodeDetailsDialog", () => {
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutPosition });
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 });
|
||||
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />);
|
||||
|
||||
expect(screen.queryByText(/Coordinates:/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Altitude:/i)).not.toBeInTheDocument();
|
||||
@@ -122,7 +126,7 @@ describe("NodeDetailsDialog", () => {
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutMetrics });
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 });
|
||||
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />);
|
||||
|
||||
expect(screen.queryByText(/Device Metrics:/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Air TX utilization:/i)).not.toBeInTheDocument();
|
||||
@@ -134,9 +138,8 @@ describe("NodeDetailsDialog", () => {
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeNeverHeard });
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 });
|
||||
|
||||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />);
|
||||
|
||||
expect(screen.getByText(/Last Heard: Never/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ export const NodeDetailsDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent >
|
||||
<DialogContent>
|
||||
<DialogClose />
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -78,9 +78,8 @@ export const NodeDetailsDialog = ({
|
||||
<div className="flex flex-col">
|
||||
<DeviceImage
|
||||
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||
deviceType={
|
||||
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
|
||||
}
|
||||
deviceType={Protobuf.Mesh
|
||||
.HardwareModel[device.user?.hwModel ?? 0]}
|
||||
/>
|
||||
<div className="bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
<p className="text-lg font-semibold">Details:</p>
|
||||
@@ -91,16 +90,14 @@ export const NodeDetailsDialog = ({
|
||||
<p>Node Number: {device.num}</p>
|
||||
<p>Node Hex: !{numberToHexUnpadded(device.num)}</p>
|
||||
<p>
|
||||
Role:{" "}
|
||||
{
|
||||
Protobuf.Config.Config_DeviceConfig_Role[
|
||||
Role: {Protobuf.Config.Config_DeviceConfig_Role[
|
||||
device.user?.role ?? 0
|
||||
]
|
||||
}
|
||||
]}
|
||||
</p>
|
||||
<p>
|
||||
Last Heard:{" "}
|
||||
{device.lastHeard === 0 ? "Never" : <TimeAgo timestamp={device.lastHeard * 1000} />}
|
||||
Last Heard: {device.lastHeard === 0
|
||||
? "Never"
|
||||
: <TimeAgo timestamp={device.lastHeard * 1000} />}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -112,9 +109,9 @@ export const NodeDetailsDialog = ({
|
||||
Coordinates:{" "}
|
||||
<a
|
||||
className="text-blue-500 dark:text-blue-400"
|
||||
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7
|
||||
}&mlon=${device.position.longitudeI / 1e7
|
||||
}&layers=N`}
|
||||
href={`https://www.openstreetmap.org/?mlat=${
|
||||
device.position.latitudeI / 1e7
|
||||
}&mlon=${device.position.longitudeI / 1e7}&layers=N`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@@ -140,7 +137,7 @@ export const NodeDetailsDialog = ({
|
||||
<p key={metric.key}>
|
||||
{metric.label}: {metric.format(metric.value)}
|
||||
</p>
|
||||
)
|
||||
),
|
||||
)}
|
||||
{device.deviceMetrics.uptimeSeconds && (
|
||||
<p>
|
||||
@@ -150,7 +147,6 @@ export const NodeDetailsDialog = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||
|
||||
@@ -13,7 +13,10 @@ 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/index.ts";
|
||||
import {
|
||||
MessageType,
|
||||
useMessageStore,
|
||||
} from "../../core/stores/messageStore/index.ts";
|
||||
|
||||
export interface NodeOptionsDialogProps {
|
||||
node: Protobuf.Mesh.NodeInfo | undefined;
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface PkiRegenerateDialogProps {
|
||||
title: string;
|
||||
description: string;
|
||||
button: string;
|
||||
}
|
||||
};
|
||||
open: boolean;
|
||||
onOpenChange: () => void;
|
||||
onSubmit: () => void;
|
||||
|
||||
@@ -79,8 +79,8 @@ export const QRDialog = ({
|
||||
{channel.settings?.name.length
|
||||
? channel.settings.name
|
||||
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
|
||||
? "Primary"
|
||||
: `Channel: ${channel.index}`}
|
||||
? "Primary"
|
||||
: `Channel: ${channel.index}`}
|
||||
</Label>
|
||||
<Checkbox
|
||||
key={channel.index}
|
||||
@@ -108,20 +108,22 @@ export const QRDialog = ({
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className={`border-slate-900 border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-hidden focus:ring-2 focus:ring-offset-2 ${qrCodeAdd
|
||||
? "focus:ring-green-800 bg-green-800 text-white"
|
||||
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||
}`}
|
||||
className={`border-slate-900 border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-hidden focus:ring-2 focus:ring-offset-2 ${
|
||||
qrCodeAdd
|
||||
? "focus:ring-green-800 bg-green-800 text-white"
|
||||
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||
}`}
|
||||
onClick={() => setQrCodeAdd(true)}
|
||||
>
|
||||
Add Channels
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`border-slate-900 border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-hidden focus:ring-2 focus:ring-offset-2 ${!qrCodeAdd
|
||||
? "focus:ring-green-800 bg-green-800 text-white"
|
||||
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||
}`}
|
||||
className={`border-slate-900 border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-hidden focus:ring-2 focus:ring-offset-2 ${
|
||||
!qrCodeAdd
|
||||
? "focus:ring-green-800 bg-green-800 text-white"
|
||||
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
|
||||
}`}
|
||||
onClick={() => setQrCodeAdd(false)}
|
||||
>
|
||||
Replace Channels
|
||||
@@ -134,7 +136,7 @@ export const QRDialog = ({
|
||||
value={qrCodeUrl}
|
||||
disabled
|
||||
action={{
|
||||
key: 'copy-value',
|
||||
key: "copy-value",
|
||||
icon: ClipboardIcon,
|
||||
onClick() {
|
||||
void navigator.clipboard.writeText(qrCodeUrl);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { RebootOTADialog } from './RebootOTADialog.tsx';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { RebootOTADialog } from "./RebootOTADialog.tsx";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const rebootOtaMock = vi.fn();
|
||||
@@ -8,41 +8,46 @@ let mockConnection: { rebootOta: (delay: number) => void } | undefined = {
|
||||
rebootOta: rebootOtaMock,
|
||||
};
|
||||
|
||||
vi.mock('@core/stores/deviceStore.ts', () => ({
|
||||
vi.mock("@core/stores/deviceStore.ts", () => ({
|
||||
useDevice: () => ({
|
||||
connection: mockConnection,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@components/UI/Button.tsx', async () => {
|
||||
const actual = await vi.importActual('@components/UI/Button.tsx');
|
||||
vi.mock("@components/UI/Button.tsx", async () => {
|
||||
const actual = await vi.importActual("@components/UI/Button.tsx");
|
||||
return {
|
||||
...actual,
|
||||
Button: (props: any) => <button {...props} />,
|
||||
Button: (props) => <button {...props} />,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@components/UI/Input.tsx', async () => {
|
||||
const actual = await vi.importActual('@components/UI/Input.tsx');
|
||||
vi.mock("@components/UI/Input.tsx", async () => {
|
||||
const actual = await vi.importActual("@components/UI/Input.tsx");
|
||||
return {
|
||||
...actual,
|
||||
Input: (props: any) => <input {...props} />,
|
||||
Input: (props) => <input {...props} />,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@components/UI/Dialog.tsx', () => {
|
||||
vi.mock("@components/UI/Dialog.tsx", () => {
|
||||
return {
|
||||
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogContent: ({ children }: { children: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DialogHeader: ({ children }: { children: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>,
|
||||
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
|
||||
DialogDescription: ({ children }: { children: ReactNode }) => (
|
||||
<p>{children}</p>
|
||||
),
|
||||
DialogClose: () => null,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
describe('RebootOTADialog', () => {
|
||||
describe("RebootOTADialog", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
rebootOtaMock.mockClear();
|
||||
@@ -52,19 +57,19 @@ describe('RebootOTADialog', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders dialog with default input value', () => {
|
||||
render(<RebootOTADialog open={true} onOpenChange={() => { }} />);
|
||||
it("renders dialog with default input value", () => {
|
||||
render(<RebootOTADialog open onOpenChange={() => {}} />);
|
||||
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
||||
expect(screen.getByText(/schedule reboot/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/reboot to ota mode now/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('schedules a reboot with delay and calls rebootOta', async () => {
|
||||
it("schedules a reboot with delay and calls rebootOta", async () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
render(<RebootOTADialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||
target: { value: '3' },
|
||||
target: { value: "3" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText(/schedule reboot/i));
|
||||
@@ -79,9 +84,9 @@ describe('RebootOTADialog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers an instant reboot', async () => {
|
||||
it("triggers an instant reboot", async () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
render(<RebootOTADialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
||||
|
||||
fireEvent.click(screen.getByText(/reboot to ota mode now/i));
|
||||
|
||||
@@ -91,13 +96,13 @@ describe('RebootOTADialog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call reboot if connection is undefined', async () => {
|
||||
it("does not call reboot if connection is undefined", async () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
|
||||
// simulate no connection
|
||||
mockConnection = undefined;
|
||||
|
||||
render(<RebootOTADialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
||||
|
||||
fireEvent.click(screen.getByText(/schedule reboot/i));
|
||||
vi.advanceTimersByTime(5000);
|
||||
@@ -110,5 +115,4 @@ describe('RebootOTADialog', () => {
|
||||
// reset connection for other tests
|
||||
mockConnection = { rebootOta: rebootOtaMock };
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -19,7 +19,9 @@ export interface RebootOTADialogProps {
|
||||
|
||||
const DEFAULT_REBOOT_DELAY = 5; // seconds
|
||||
|
||||
export const RebootOTADialog = ({ open, onOpenChange }: RebootOTADialogProps) => {
|
||||
export const RebootOTADialog = (
|
||||
{ open, onOpenChange }: RebootOTADialogProps,
|
||||
) => {
|
||||
const { connection } = useDevice();
|
||||
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
|
||||
const [isScheduled, setIsScheduled] = useState(false);
|
||||
@@ -28,8 +30,8 @@ export const RebootOTADialog = ({ open, onOpenChange }: RebootOTADialogProps) =>
|
||||
const handleSetTime = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.validity.valid) {
|
||||
e.preventDefault();
|
||||
return
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const val = e.target.value;
|
||||
setInputValue(val);
|
||||
@@ -73,7 +75,8 @@ export const RebootOTADialog = ({ open, onOpenChange }: RebootOTADialogProps) =>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reboot to OTA Mode</DialogTitle>
|
||||
<DialogDescription>
|
||||
Reboot the connected node after a delay into OTA (Over-the-Air) mode.
|
||||
Reboot the connected node after a delay into OTA (Over-the-Air)
|
||||
mode.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -89,7 +92,7 @@ export const RebootOTADialog = ({ open, onOpenChange }: RebootOTADialogProps) =>
|
||||
/>
|
||||
<Button onClick={() => handleRebootWithTimeout()} className="w-9/12">
|
||||
<ClockIcon className="mr-2" size={18} />
|
||||
{isScheduled ? 'Reboot has been scheduled' : 'Schedule Reboot'}
|
||||
{isScheduled ? "Reboot has been scheduled" : "Schedule Reboot"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -101,4 +104,3 @@ export const RebootOTADialog = ({ open, onOpenChange }: RebootOTADialogProps) =>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { expect, test, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
vi.mock("@core/stores/messageStore");
|
||||
@@ -12,7 +12,9 @@ vi.mock("./useRefreshKeysDialog");
|
||||
const mockUseMessageStore = vi.mocked(useMessageStore);
|
||||
const mockUseRefreshKeysDialog = vi.mocked(useRefreshKeysDialog);
|
||||
|
||||
const getInitialState = () => useDeviceStore.getInitialState?.() ?? { devices: new Map(), remoteDevices: new Map() };
|
||||
const getInitialState = () =>
|
||||
useDeviceStore.getInitialState?.() ??
|
||||
{ devices: new Map(), remoteDevices: new Map() };
|
||||
|
||||
beforeEach(() => {
|
||||
useDeviceStore.setState(getInitialState(), true);
|
||||
@@ -39,17 +41,19 @@ test("renders dialog when there is a node error for the active chat", () => {
|
||||
longName: "Problem Node Long",
|
||||
shortName: "ProbNode",
|
||||
isLicensed: false,
|
||||
macaddr: new Uint8Array(0)
|
||||
macaddr: new Uint8Array(0),
|
||||
},
|
||||
lastHeard: Date.now() / 1000,
|
||||
snr: 10
|
||||
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");
|
||||
throw new Error(
|
||||
"Failed to get updated device state from store for provider",
|
||||
);
|
||||
}
|
||||
|
||||
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
|
||||
@@ -63,12 +67,18 @@ test("renders dialog when there is a node error for the active chat", () => {
|
||||
render(
|
||||
<DeviceContext.Provider value={updatedDeviceState}>
|
||||
<RefreshKeysDialog open onOpenChange={vi.fn()} />
|
||||
</DeviceContext.Provider>
|
||||
</DeviceContext.Provider>,
|
||||
);
|
||||
|
||||
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.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();
|
||||
});
|
||||
|
||||
@@ -90,7 +100,7 @@ test("does not render dialog if no error exists for active chat", () => {
|
||||
const { container } = render(
|
||||
<DeviceContext.Provider value={currentDeviceState}>
|
||||
<RefreshKeysDialog open onOpenChange={vi.fn()} />
|
||||
</DeviceContext.Provider>
|
||||
</DeviceContext.Provider>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
|
||||
@@ -16,7 +16,9 @@ export interface RefreshKeysDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
|
||||
export const RefreshKeysDialog = (
|
||||
{ open, onOpenChange }: RefreshKeysDialogProps,
|
||||
) => {
|
||||
const { activeChat } = useMessageStore();
|
||||
const { nodeErrors, getNode } = useDevice();
|
||||
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
|
||||
@@ -31,8 +33,12 @@ export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps
|
||||
|
||||
const text = {
|
||||
title: `Keys Mismatch - ${nodeWithError?.user?.longName ?? ""}`,
|
||||
description: `Your node is unable to send a direct message to node: ${nodeWithError?.user?.longName ?? ""} (${nodeWithError?.user?.shortName ?? ""}). This is due to the remote node's current public key does not match the previously stored key for this node.`,
|
||||
}
|
||||
description: `Your node is unable to send a direct message to node: ${
|
||||
nodeWithError?.user?.longName ?? ""
|
||||
} (${
|
||||
nodeWithError?.user?.shortName ?? ""
|
||||
}). This is due to the remote node's current public key does not match the previously stored key for this node.`,
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-8 flex flex-col gap-2">
|
||||
@@ -44,7 +50,10 @@ export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps
|
||||
<ul className="mt-2">
|
||||
<li className="flex place-items-center gap-2 items-start">
|
||||
<div className="p-2 bg-slate-500 rounded-lg mt-1">
|
||||
<LockKeyholeOpenIcon size={30} className="text-white justify-center" />
|
||||
<LockKeyholeOpenIcon
|
||||
size={30}
|
||||
className="text-white justify-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
@@ -70,6 +79,6 @@ export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps
|
||||
</ul>
|
||||
{/* </DialogDescription> */}
|
||||
</DialogContent>
|
||||
</Dialog >
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { act, renderHook } 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";
|
||||
@@ -38,7 +38,7 @@ describe("useRefreshKeysDialog Hook", () => {
|
||||
});
|
||||
|
||||
vi.mocked(useMessageStore).mockReturnValue({
|
||||
activeChat: "chat-123"
|
||||
activeChat: "chat-123",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +46,9 @@ describe("useRefreshKeysDialog Hook", () => {
|
||||
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");
|
||||
@@ -60,7 +62,9 @@ describe("useRefreshKeysDialog Hook", () => {
|
||||
|
||||
it("handleNodeRemove should do nothing if there is no error", () => {
|
||||
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||
act(() => { result.current.handleNodeRemove(); });
|
||||
act(() => {
|
||||
result.current.handleNodeRemove();
|
||||
});
|
||||
|
||||
expect(getNodeErrorMock).toHaveBeenCalledTimes(1);
|
||||
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||
@@ -79,4 +83,4 @@ describe("useRefreshKeysDialog Hook", () => {
|
||||
expect(setDialogOpenMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,12 @@ import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
||||
|
||||
export function useRefreshKeysDialog() {
|
||||
const { removeNode, setDialogOpen, clearNodeError, getNodeError } = useDevice();
|
||||
const { removeNode, setDialogOpen, clearNodeError, getNodeError } =
|
||||
useDevice();
|
||||
const { activeChat } = useMessageStore();
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setDialogOpen('refreshKeys', false);
|
||||
setDialogOpen("refreshKeys", false);
|
||||
}, [setDialogOpen]);
|
||||
|
||||
const handleNodeRemove = useCallback(() => {
|
||||
@@ -22,6 +23,6 @@ export function useRefreshKeysDialog() {
|
||||
|
||||
return {
|
||||
handleCloseDialog,
|
||||
handleNodeRemove
|
||||
handleNodeRemove,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ export const TracerouteResponseDialog = ({
|
||||
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 snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
|
||||
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
|
||||
const from = getNode(traceroute?.from ?? 0);
|
||||
const longName = from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// deno-lint-ignore-file
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
||||
@@ -10,41 +10,58 @@ describe("UnsafeRolesDialog", () => {
|
||||
setDialogOpen: vi.fn(),
|
||||
};
|
||||
|
||||
const renderWithDeviceContext = (ui: any) => {
|
||||
const renderWithDeviceContext = (ui: React.ReactNode) => {
|
||||
return render(
|
||||
<DeviceWrapper device={mockDevice}>
|
||||
{ui}
|
||||
</DeviceWrapper>
|
||||
</DeviceWrapper>,
|
||||
);
|
||||
};
|
||||
|
||||
it("renders the dialog when open is true", () => {
|
||||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||
renderWithDeviceContext(
|
||||
<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog');
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(dialog).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/I have read the/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/understand the implications/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/understand the implications/i))
|
||||
.toBeInTheDocument();
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
const links = screen.getAllByRole("link");
|
||||
expect(links).toHaveLength(2);
|
||||
expect(links[0]).toHaveTextContent('Device Role Documentation');
|
||||
expect(links[1]).toHaveTextContent('Choosing The Right Device Role');
|
||||
expect(links[0]).toHaveTextContent("Device Role Documentation");
|
||||
expect(links[1]).toHaveTextContent("Choosing The Right Device Role");
|
||||
});
|
||||
|
||||
it("displays the correct links", () => {
|
||||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||
renderWithDeviceContext(
|
||||
<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
const docLink = screen.getByRole("link", { name: /Device Role Documentation/i });
|
||||
const blogLink = screen.getByRole("link", { name: /Choosing The Right Device Role/i });
|
||||
const docLink = screen.getByRole("link", {
|
||||
name: /Device Role Documentation/i,
|
||||
});
|
||||
const blogLink = screen.getByRole("link", {
|
||||
name: /Choosing The Right Device Role/i,
|
||||
});
|
||||
|
||||
expect(docLink).toHaveAttribute("href", "https://meshtastic.org/docs/configuration/radio/device/");
|
||||
expect(blogLink).toHaveAttribute("href", "https://meshtastic.org/blog/choosing-the-right-device-role/");
|
||||
expect(docLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://meshtastic.org/docs/configuration/radio/device/",
|
||||
);
|
||||
expect(blogLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://meshtastic.org/blog/choosing-the-right-device-role/",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not allow confirmation until checkbox is checked", () => {
|
||||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||
renderWithDeviceContext(
|
||||
<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
|
||||
@@ -58,27 +75,37 @@ describe("UnsafeRolesDialog", () => {
|
||||
|
||||
it("emits the correct event when closing via close button", () => {
|
||||
const eventSpy = vi.spyOn(eventBus, "emit");
|
||||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||
renderWithDeviceContext(
|
||||
<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
const dismissButton = screen.getByRole("button", { name: /close/i });
|
||||
fireEvent.click(dismissButton);
|
||||
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "dismiss" });
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", {
|
||||
action: "dismiss",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits the correct event when dismissing", () => {
|
||||
const eventSpy = vi.spyOn(eventBus, "emit");
|
||||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||
renderWithDeviceContext(
|
||||
<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
const dismissButton = screen.getByRole("button", { name: /dismiss/i });
|
||||
fireEvent.click(dismissButton);
|
||||
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "dismiss" });
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", {
|
||||
action: "dismiss",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits the correct event when confirming", () => {
|
||||
const eventSpy = vi.spyOn(eventBus, "emit");
|
||||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||
renderWithDeviceContext(
|
||||
<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
@@ -86,6 +113,8 @@ describe("UnsafeRolesDialog", () => {
|
||||
fireEvent.click(checkbox);
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "confirm" });
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", {
|
||||
action: "confirm",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,29 +19,40 @@ export interface RouterRoleDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps) => {
|
||||
export const UnsafeRolesDialog = (
|
||||
{ open, onOpenChange }: RouterRoleDialogProps,
|
||||
) => {
|
||||
const [confirmState, setConfirmState] = useState(false);
|
||||
const { setDialogOpen } = useDevice();
|
||||
|
||||
const deviceRoleLink = "https://meshtastic.org/docs/configuration/radio/device/";
|
||||
const choosingTheRightDeviceRoleLink = "https://meshtastic.org/blog/choosing-the-right-device-role/";
|
||||
const deviceRoleLink =
|
||||
"https://meshtastic.org/docs/configuration/radio/device/";
|
||||
const choosingTheRightDeviceRoleLink =
|
||||
"https://meshtastic.org/blog/choosing-the-right-device-role/";
|
||||
|
||||
const handleCloseDialog = (action: 'confirm' | 'dismiss') => {
|
||||
setDialogOpen('unsafeRoles', false);
|
||||
const handleCloseDialog = (action: "confirm" | "dismiss") => {
|
||||
setDialogOpen("unsafeRoles", false);
|
||||
setConfirmState(false);
|
||||
eventBus.emit('dialog:unsafeRoles', { action });
|
||||
}
|
||||
eventBus.emit("dialog:unsafeRoles", { action });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-8 flex flex-col">
|
||||
<DialogClose onClick={() => handleCloseDialog('dismiss')} />
|
||||
<DialogClose onClick={() => handleCloseDialog("dismiss")} />
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="text-md">
|
||||
I have read the <Link href={deviceRoleLink} className="">Device Role Documentation</Link>{" "}
|
||||
and the blog post about <Link href={choosingTheRightDeviceRoleLink}>Choosing The Right Device Role</Link> and understand the implications of changing the role.
|
||||
I have read the{" "}
|
||||
<Link href={deviceRoleLink} className="">
|
||||
Device Role Documentation
|
||||
</Link>{" "}
|
||||
and the blog post about{" "}
|
||||
<Link href={choosingTheRightDeviceRoleLink}>
|
||||
Choosing The Right Device Role
|
||||
</Link>{" "}
|
||||
and understand the implications of changing the role.
|
||||
</DialogDescription>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
@@ -56,16 +67,20 @@ export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps)
|
||||
<Button
|
||||
variant="default"
|
||||
name="dismiss"
|
||||
onClick={() => handleCloseDialog('dismiss')}> Dismiss
|
||||
onClick={() => handleCloseDialog("dismiss")}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
name="confirm"
|
||||
disabled={!confirmState}
|
||||
onClick={() => handleCloseDialog('confirm')}> Confirm
|
||||
onClick={() => handleCloseDialog("confirm")}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog >
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useUnsafeRolesDialog, UNSAFE_ROLES } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import {
|
||||
UNSAFE_ROLES,
|
||||
useUnsafeRolesDialog,
|
||||
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
|
||||
vi.mock('@core/utils/eventBus', () => ({
|
||||
vi.mock("@core/utils/eventBus", () => ({
|
||||
eventBus: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
@@ -15,13 +18,13 @@ const mockDevice = {
|
||||
setDialogOpen: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('@core/stores/deviceStore', () => ({
|
||||
vi.mock("@core/stores/deviceStore", () => ({
|
||||
useDevice: () => ({
|
||||
setDialogOpen: mockDevice.setDialogOpen,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useUnsafeRolesDialog', () => {
|
||||
describe("useUnsafeRolesDialog", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
@@ -34,84 +37,115 @@ describe('useUnsafeRolesDialog', () => {
|
||||
return renderHook(() => useUnsafeRolesDialog());
|
||||
};
|
||||
|
||||
describe('handleCloseDialog', () => {
|
||||
it('should call setDialogOpen with correct parameters when dialog is closed', () => {
|
||||
describe("handleCloseDialog", () => {
|
||||
it("should call setDialogOpen with correct parameters when dialog is closed", () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
result.current.handleCloseDialog();
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith('unsafeRoles', false);
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
"unsafeRoles",
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRoleSelection', () => {
|
||||
it('should resolve with true for safe roles without opening dialog', async () => {
|
||||
describe("validateRoleSelection", () => {
|
||||
it("should resolve with true for safe roles without opening dialog", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
const safeRole = 'SAFE_ROLE';
|
||||
const safeRole = "SAFE_ROLE";
|
||||
|
||||
const validationResult = await result.current.validateRoleSelection(safeRole);
|
||||
const validationResult = await result.current.validateRoleSelection(
|
||||
safeRole,
|
||||
);
|
||||
|
||||
expect(validationResult).toBe(true);
|
||||
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open dialog for unsafe roles and resolve with true when confirmed', async () => {
|
||||
it("should open dialog for unsafe roles and resolve with true when confirmed", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]);
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[0],
|
||||
);
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith('unsafeRoles', true);
|
||||
expect(eventBus.on).toHaveBeenCalledWith('dialog:unsafeRoles', expect.any(Function));
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
"unsafeRoles",
|
||||
true,
|
||||
);
|
||||
expect(eventBus.on).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
onHandler({ action: 'confirm' });
|
||||
onHandler({ action: "confirm" });
|
||||
const validationResult = await validationPromise;
|
||||
|
||||
expect(validationResult).toBe(true);
|
||||
expect(eventBus.off).toHaveBeenCalledWith('dialog:unsafeRoles', onHandler);
|
||||
expect(eventBus.off).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
onHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve with false when user dismisses the dialog', async () => {
|
||||
it("should resolve with false when user dismisses the dialog", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]);
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[0],
|
||||
);
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
onHandler({ action: 'dismiss' });
|
||||
onHandler({ action: "dismiss" });
|
||||
|
||||
const validationResult = await validationPromise;
|
||||
expect(validationResult).toBe(false);
|
||||
expect(eventBus.off).toHaveBeenCalledWith('dialog:unsafeRoles', onHandler);
|
||||
expect(eventBus.off).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
onHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should clean up event listener after response', async () => {
|
||||
it("should clean up event listener after response", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[1]);
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[1],
|
||||
);
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
|
||||
onHandler({ action: 'confirm' });
|
||||
onHandler({ action: "confirm" });
|
||||
await validationPromise;
|
||||
|
||||
expect(eventBus.off).toHaveBeenCalledWith('dialog:unsafeRoles', onHandler);
|
||||
expect(eventBus.off).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
onHandler,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with all unsafe roles', async () => {
|
||||
it("should work with all unsafe roles", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
for (const unsafeRole of UNSAFE_ROLES) {
|
||||
mockDevice.setDialogOpen.mockClear();
|
||||
(eventBus.on as Mock).mockClear();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(unsafeRole);
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
unsafeRole,
|
||||
);
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith('unsafeRoles', true);
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
"unsafeRoles",
|
||||
true,
|
||||
);
|
||||
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
onHandler({ action: 'confirm' });
|
||||
onHandler({ action: "confirm" });
|
||||
|
||||
const validationResult = await validationPromise;
|
||||
|
||||
expect(validationResult).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,9 @@ export const useUnsafeRolesDialog = () => {
|
||||
setDialogOpen("unsafeRoles", true);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const handleResponse = ({ action }: { action: "confirm" | "dismiss" }) => {
|
||||
const handleResponse = (
|
||||
{ action }: { action: "confirm" | "dismiss" },
|
||||
) => {
|
||||
eventBus.off("dialog:unsafeRoles", handleResponse);
|
||||
resolve(action === "confirm");
|
||||
};
|
||||
@@ -29,7 +31,7 @@ export const useUnsafeRolesDialog = () => {
|
||||
eventBus.on("dialog:unsafeRoles", handleResponse);
|
||||
});
|
||||
},
|
||||
[setDialogOpen]
|
||||
[setDialogOpen],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user