refactor: fixed unsafe roles dialog and hook logic, added tests
This commit is contained in:
@@ -1,88 +1,91 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
// deno-lint-ignore-file
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { UnsafeRolesDialog } from '@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx';
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { useUnsafeRoles } from '@components/Dialog/UnsafeRolesDialog/useUnsafeRoles.ts';
|
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
||||||
|
import { eventBus } from "@core/utils/eventBus.ts";
|
||||||
|
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
|
||||||
|
|
||||||
vi.mock('@components/Dialog/UnsafeRolesDialog/useUnsafeRoles', () => ({
|
describe("UnsafeRolesDialog", () => {
|
||||||
useUnsafeRoles: vi.fn()
|
const mockDevice = {
|
||||||
}));
|
setDialogOpen: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
describe('UnsafeRolesDialog', () => {
|
const renderWithDeviceContext = (ui: any) => {
|
||||||
const getConfirmStateMock = vi.fn();
|
return render(
|
||||||
const toggleConfirmStateMock = vi.fn();
|
<DeviceWrapper device={mockDevice}>
|
||||||
const handleCloseDialogMock = vi.fn();
|
{ui}
|
||||||
const onOpenChangeMock = vi.fn();
|
</DeviceWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
it("renders the dialog when open is true", () => {
|
||||||
vi.resetAllMocks();
|
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||||
|
|
||||||
getConfirmStateMock.mockReturnValue(false);
|
const dialog = screen.getByRole('dialog');
|
||||||
|
expect(dialog).toBeInTheDocument();
|
||||||
|
|
||||||
(useUnsafeRoles as any).mockReturnValue({
|
expect(screen.getByText(/I have read the/i)).toBeInTheDocument();
|
||||||
getConfirmState: getConfirmStateMock,
|
expect(screen.getByText(/understand the implications/i)).toBeInTheDocument();
|
||||||
toggleConfirmState: toggleConfirmStateMock,
|
|
||||||
handleCloseDialog: handleCloseDialogMock
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render when open is false', () => {
|
it("displays the correct links", () => {
|
||||||
render(<UnsafeRolesDialog open={false} onOpenChange={onOpenChangeMock} />);
|
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||||
|
|
||||||
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
|
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/");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render when open is true', () => {
|
it("does not allow confirmation until checkbox is checked", () => {
|
||||||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />);
|
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||||
|
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||||
expect(screen.getByRole('heading')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Are you sure?')).toBeInTheDocument();
|
expect(confirmButton).toBeDisabled();
|
||||||
expect(screen.getAllByRole('link')).length(2);
|
|
||||||
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
const checkbox = screen.getByRole("checkbox");
|
||||||
expect(screen.getByText('Yes, I know what I\'m doing')).toBeInTheDocument();
|
fireEvent.click(checkbox);
|
||||||
expect(screen.getByRole('button', { name: /dismiss/i })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument();
|
expect(confirmButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have disabled confirm button when checkbox is unchecked', () => {
|
it("emits the correct event when closing via close button", () => {
|
||||||
getConfirmStateMock.mockReturnValue(false);
|
const eventSpy = vi.spyOn(eventBus, "emit");
|
||||||
|
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||||
|
|
||||||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />);
|
const dismissButton = screen.getByRole("button", { name: /close/i });
|
||||||
|
fireEvent.click(dismissButton);
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /confirm/i })).toBeDisabled();
|
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "dismiss" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have enabled confirm button when checkbox is checked', () => {
|
it("emits the correct event when dismissing", () => {
|
||||||
getConfirmStateMock.mockReturnValue(true);
|
const eventSpy = vi.spyOn(eventBus, "emit");
|
||||||
|
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||||
|
|
||||||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />);
|
const dismissButton = screen.getByRole("button", { name: /dismiss/i });
|
||||||
|
fireEvent.click(dismissButton);
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /confirm/i })).not.toBeDisabled();
|
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "dismiss" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call toggleConfirmState when checkbox is clicked', () => {
|
it("emits the correct event when confirming", () => {
|
||||||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />);
|
const eventSpy = vi.spyOn(eventBus, "emit");
|
||||||
|
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('checkbox'));
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||||
|
|
||||||
expect(toggleConfirmStateMock).toHaveBeenCalledTimes(1);
|
fireEvent.click(checkbox);
|
||||||
});
|
fireEvent.click(confirmButton);
|
||||||
|
|
||||||
it('should call handleCloseDialog with "dismiss" when dismiss button is clicked', () => {
|
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "confirm" });
|
||||||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /dismiss/i }));
|
|
||||||
|
|
||||||
expect(handleCloseDialogMock).toHaveBeenCalledWith('dismiss');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call handleCloseDialog with "confirm" when confirm button is clicked', () => {
|
|
||||||
getConfirmStateMock.mockReturnValue(true);
|
|
||||||
|
|
||||||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /confirm/i }));
|
|
||||||
|
|
||||||
expect(handleCloseDialogMock).toHaveBeenCalledWith("confirm");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
@@ -7,10 +8,11 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||||
import { Checkbox } from "../../UI/Checkbox/index.tsx";
|
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||||
import { Label } from "@components/UI/Label.tsx";
|
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { useUnsafeRoles } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRoles.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { eventBus } from "@core/utils/eventBus.ts";
|
||||||
|
|
||||||
export interface RouterRoleDialogProps {
|
export interface RouterRoleDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -18,31 +20,49 @@ export interface RouterRoleDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps) => {
|
export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps) => {
|
||||||
const { getConfirmState, toggleConfirmState, handleCloseDialog } = useUnsafeRoles();
|
const [confirmState, setConfirmState] = useState(false);
|
||||||
|
const { setDialogOpen } = useDevice();
|
||||||
|
|
||||||
const deivceRoleLink = "https://meshtastic.org/docs/configuration/radio/device/";
|
const deviceRoleLink = "https://meshtastic.org/docs/configuration/radio/device/";
|
||||||
const choosingTheRightDeviceRoleLink = "https://meshtastic.org/blog/choosing-the-right-device-role/";
|
const choosingTheRightDeviceRoleLink = "https://meshtastic.org/blog/choosing-the-right-device-role/";
|
||||||
|
|
||||||
|
const handleCloseDialog = (action: 'confirm' | 'dismiss') => {
|
||||||
|
setDialogOpen('unsafeRoles', false);
|
||||||
|
setConfirmState(false);
|
||||||
|
eventBus.emit('dialog:unsafeRoles', { action });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-8 flex flex-col">
|
<DialogContent className="max-w-8 flex flex-col">
|
||||||
|
<DialogClose onClick={() => handleCloseDialog('dismiss')} />
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Are you sure?</DialogTitle>
|
<DialogTitle>Are you sure?</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription className="text-md">
|
<DialogDescription className="text-md">
|
||||||
I have read the <Link href={deivceRoleLink} className="">Device Role Documentation</Link>{" "}
|
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.
|
and the blog post about <Link href={choosingTheRightDeviceRoleLink}>Choosing The Right Device Role</Link> and understand the implications of changing the role.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox id="routerRole" checked={getConfirmState()} onChange={toggleConfirmState}>
|
<Checkbox
|
||||||
|
id="routerRole"
|
||||||
|
checked={confirmState}
|
||||||
|
onChange={() => setConfirmState(!confirmState)}
|
||||||
|
>
|
||||||
Yes, I know what I'm doing
|
Yes, I know what I'm doing
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="mt-6">
|
<DialogFooter className="mt-6">
|
||||||
<Button variant="default" name="dismiss" onClick={() => handleCloseDialog("dismiss")}>
|
<Button
|
||||||
Dismiss
|
variant="default"
|
||||||
|
name="dismiss"
|
||||||
|
onClick={() => handleCloseDialog('dismiss')}> Dismiss
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="default" name="confirm" disabled={!getConfirmState()} onClick={() => handleCloseDialog("confirm")}>
|
<Button
|
||||||
Confirm
|
variant="default"
|
||||||
|
name="confirm"
|
||||||
|
disabled={!confirmState}
|
||||||
|
onClick={() => handleCloseDialog('confirm')}> Confirm
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
||||||
import { renderHook, act } from '@testing-library/react';
|
|
||||||
import { useUnsafeRoles } from './useUnsafeRoles.ts';
|
|
||||||
import { useDevice } from '@core/stores/deviceStore.ts';
|
|
||||||
import useLocalStorage from '@core/hooks/useLocalStorage.ts';
|
|
||||||
|
|
||||||
vi.mock('@core/stores/deviceStore', () => ({
|
|
||||||
useDevice: vi.fn()
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('@core/hooks/useLocalStorage', () => {
|
|
||||||
return {
|
|
||||||
default: vi.fn()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useUnsafeRoles', () => {
|
|
||||||
const setDialogOpenMock = vi.fn();
|
|
||||||
const setAgreedToUnsafeRolesMock = vi.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
|
|
||||||
(useDevice as any).mockReturnValue({
|
|
||||||
setDialogOpen: setDialogOpenMock
|
|
||||||
});
|
|
||||||
|
|
||||||
(useLocalStorage as any).mockReturnValue([
|
|
||||||
false,
|
|
||||||
setAgreedToUnsafeRolesMock
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize with correct default values', () => {
|
|
||||||
const { result } = renderHook(() => useUnsafeRoles());
|
|
||||||
|
|
||||||
expect(result.current.agreedToUnSafeRoles).toBe(false);
|
|
||||||
expect(result.current.getConfirmState()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should toggle confirm state correctly', () => {
|
|
||||||
const { result } = renderHook(() => useUnsafeRoles());
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.toggleConfirmState();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.getConfirmState()).toBe(true);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.toggleConfirmState();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.getConfirmState()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle dialog close with dismiss state', () => {
|
|
||||||
const { result } = renderHook(() => useUnsafeRoles());
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.handleCloseDialog('dismiss');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(setAgreedToUnsafeRolesMock).toHaveBeenCalledWith(false);
|
|
||||||
expect(setDialogOpenMock).toHaveBeenCalledWith('unsafeRoles', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle dialog close with confirm state', () => {
|
|
||||||
const { result } = renderHook(() => useUnsafeRoles());
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.handleCloseDialog('confirm');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(setAgreedToUnsafeRolesMock).toHaveBeenCalledWith(true);
|
|
||||||
expect(setDialogOpenMock).toHaveBeenCalledWith('unsafeRoles', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should maintain state consistency across multiple operations', () => {
|
|
||||||
const { result } = renderHook(() => useUnsafeRoles());
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.toggleConfirmState();
|
|
||||||
});
|
|
||||||
expect(result.current.getConfirmState()).toBe(true);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.handleCloseDialog('confirm');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.getConfirmState()).toBe(false);
|
|
||||||
expect(setAgreedToUnsafeRolesMock).toHaveBeenCalledWith(true);
|
|
||||||
|
|
||||||
(useLocalStorage as any).mockReturnValue([
|
|
||||||
true,
|
|
||||||
setAgreedToUnsafeRolesMock
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { result: newResult } = renderHook(() => useUnsafeRoles());
|
|
||||||
expect(newResult.current.agreedToUnSafeRoles).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { useState, useCallback } from "react";
|
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
|
||||||
import useLocalStorage from "@core/hooks/useLocalStorage.ts";
|
|
||||||
|
|
||||||
export const useUnsafeRoles = () => {
|
|
||||||
const [agreedToUnSafeRoles, setAgreedToUnsafeRoles] = useLocalStorage("agreeToUnsafeRole", false);
|
|
||||||
const [_confirmState, _setConfirmState] = useState(false);
|
|
||||||
const { setDialogOpen } = useDevice();
|
|
||||||
|
|
||||||
const toggleConfirmState = useCallback(() => {
|
|
||||||
setConfirmState(!_confirmState);
|
|
||||||
}, [_confirmState]);
|
|
||||||
|
|
||||||
const setConfirmState = useCallback((state: boolean) => {
|
|
||||||
_setConfirmState(state);
|
|
||||||
}, [_setConfirmState]);
|
|
||||||
|
|
||||||
const getConfirmState = useCallback(() => {
|
|
||||||
return _confirmState;
|
|
||||||
}, [_confirmState]);
|
|
||||||
|
|
||||||
const handleCloseDialog = useCallback((closeState: "dismiss" | "confirm") => {
|
|
||||||
if (closeState === "dismiss") {
|
|
||||||
setAgreedToUnsafeRoles(false);
|
|
||||||
setConfirmState(false);
|
|
||||||
}
|
|
||||||
if (closeState === "confirm") {
|
|
||||||
setAgreedToUnsafeRoles(true);
|
|
||||||
setConfirmState(false);
|
|
||||||
}
|
|
||||||
setDialogOpen("unsafeRoles", false);
|
|
||||||
}, [setDialogOpen, setAgreedToUnsafeRoles]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
getConfirmState,
|
|
||||||
toggleConfirmState,
|
|
||||||
handleCloseDialog,
|
|
||||||
agreedToUnSafeRoles
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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";
|
||||||
|
import { eventBus } from "@core/utils/eventBus";
|
||||||
|
|
||||||
|
vi.mock('@core/utils/eventBus', () => ({
|
||||||
|
eventBus: {
|
||||||
|
on: vi.fn(),
|
||||||
|
off: vi.fn(),
|
||||||
|
emit: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockDevice = {
|
||||||
|
setDialogOpen: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('@core/stores/deviceStore', () => ({
|
||||||
|
useDevice: () => ({
|
||||||
|
setDialogOpen: mockDevice.setDialogOpen,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useUnsafeRolesDialog', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderUnsafeRolesHook = () => {
|
||||||
|
return renderHook(() => useUnsafeRolesDialog());
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateRoleSelection', () => {
|
||||||
|
it('should resolve with true for safe roles without opening dialog', async () => {
|
||||||
|
const { result } = renderUnsafeRolesHook();
|
||||||
|
const safeRole = 'SAFE_ROLE';
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
const { result } = renderUnsafeRolesHook();
|
||||||
|
|
||||||
|
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]);
|
||||||
|
|
||||||
|
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' });
|
||||||
|
const validationResult = await validationPromise;
|
||||||
|
|
||||||
|
expect(validationResult).toBe(true);
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith('dialog:unsafeRoles', onHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve with false when user dismisses the dialog', async () => {
|
||||||
|
const { result } = renderUnsafeRolesHook();
|
||||||
|
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]);
|
||||||
|
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||||
|
onHandler({ action: 'dismiss' });
|
||||||
|
|
||||||
|
const validationResult = await validationPromise;
|
||||||
|
expect(validationResult).toBe(false);
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith('dialog:unsafeRoles', onHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clean up event listener after response', async () => {
|
||||||
|
const { result } = renderUnsafeRolesHook();
|
||||||
|
|
||||||
|
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[1]);
|
||||||
|
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||||
|
|
||||||
|
onHandler({ action: 'confirm' });
|
||||||
|
await validationPromise;
|
||||||
|
|
||||||
|
expect(eventBus.off).toHaveBeenCalledWith('dialog:unsafeRoles', onHandler);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith('unsafeRoles', true);
|
||||||
|
|
||||||
|
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||||
|
onHandler({ action: 'confirm' });
|
||||||
|
|
||||||
|
const validationResult = await validationPromise;
|
||||||
|
|
||||||
|
expect(validationResult).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { eventBus } from "@core/utils/eventBus.ts";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
|
export const UNSAFE_ROLES = ["ROUTER", "REPEATER"];
|
||||||
|
export type UnsafeRole = typeof UNSAFE_ROLES[number];
|
||||||
|
|
||||||
|
export const useUnsafeRolesDialog = () => {
|
||||||
|
const { setDialogOpen } = useDevice();
|
||||||
|
|
||||||
|
const handleCloseDialog = useCallback(() => {
|
||||||
|
setDialogOpen("unsafeRoles", false);
|
||||||
|
}, [setDialogOpen]);
|
||||||
|
|
||||||
|
const validateRoleSelection = useCallback(
|
||||||
|
(newRoleKey: string): Promise<boolean> => {
|
||||||
|
if (!UNSAFE_ROLES.includes(newRoleKey as UnsafeRole)) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDialogOpen("unsafeRoles", true);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const handleResponse = ({ action }: { action: "confirm" | "dismiss" }) => {
|
||||||
|
eventBus.off("dialog:unsafeRoles", handleResponse);
|
||||||
|
resolve(action === "confirm");
|
||||||
|
};
|
||||||
|
|
||||||
|
eventBus.on("dialog:unsafeRoles", handleResponse);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setDialogOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleCloseDialog,
|
||||||
|
validateRoleSelection,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -10,11 +10,12 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@components/UI/Select.tsx";
|
} from "@components/UI/Select.tsx";
|
||||||
import { useController, type FieldValues } from "react-hook-form";
|
import { useController, type FieldValues } from "react-hook-form";
|
||||||
|
import { computeHeadingLevel } from "@core/utils/test.tsx";
|
||||||
|
|
||||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "select";
|
type: "select";
|
||||||
selectChange?: (e: string, name: string) => void;
|
selectChange?: (e: string, name: string) => void;
|
||||||
onBeforeChange?: (newValue: string, prevValue: string) => Promise<string | false>;
|
validate?: (newValue: string) => Promise<boolean>;
|
||||||
properties: BaseFormBuilderProps<T>["properties"] & {
|
properties: BaseFormBuilderProps<T>["properties"] & {
|
||||||
enumValue: {
|
enumValue: {
|
||||||
[s: string]: string | number;
|
[s: string]: string | number;
|
||||||
@@ -23,8 +24,6 @@ export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const formatEnumDisplay = (name: string): string => {
|
const formatEnumDisplay = (name: string): string => {
|
||||||
return name
|
return name
|
||||||
.replace(/_/g, " ")
|
.replace(/_/g, " ")
|
||||||
@@ -48,14 +47,12 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
|
|
||||||
const { enumValue, formatEnumName, ...remainingProperties } = field.properties;
|
const { enumValue, formatEnumName, ...remainingProperties } = field.properties;
|
||||||
const valueToKeyMap: Record<string, string> = {};
|
const valueToKeyMap: Record<string, string> = {};
|
||||||
const keyToValueMap: Record<string, number> = {};
|
|
||||||
const optionsEnumValues: [string, number][] = [];
|
const optionsEnumValues: [string, number][] = [];
|
||||||
|
|
||||||
if (enumValue) {
|
if (enumValue) {
|
||||||
Object.entries(enumValue).forEach(([key, val]) => {
|
Object.entries(enumValue).forEach(([key, val]) => {
|
||||||
if (typeof val === "number") {
|
if (typeof val === "number") {
|
||||||
valueToKeyMap[val.toString()] = key; // Map enum value to key
|
valueToKeyMap[val.toString()] = key;
|
||||||
keyToValueMap[key] = val; // Map key to enum value
|
|
||||||
optionsEnumValues.push([key, val]);
|
optionsEnumValues.push([key, val]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -63,27 +60,17 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
|
|
||||||
const handleValueChange = async (newValue: string) => {
|
const handleValueChange = async (newValue: string) => {
|
||||||
const selectedKey = valueToKeyMap[newValue];
|
const selectedKey = valueToKeyMap[newValue];
|
||||||
if (!selectedKey) return;
|
|
||||||
|
|
||||||
if (field.onBeforeChange) {
|
if (field.validate) {
|
||||||
try {
|
const isValid = await field.validate(selectedKey);
|
||||||
const result = await field.onBeforeChange(selectedKey, valueToKeyMap[value?.toString()]);
|
if (!isValid) return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result === false) return;
|
|
||||||
const updatedValue = keyToValueMap[result];
|
|
||||||
if (updatedValue !== undefined) {
|
|
||||||
if (field.selectChange) field.selectChange(updatedValue.toString(), result);
|
|
||||||
onChange(updatedValue);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in onBeforeChange function:", error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (field.selectChange) field.selectChange(newValue, selectedKey);
|
if (field.selectChange) field.selectChange(newValue, selectedKey);
|
||||||
onChange(Number.parseInt(newValue));
|
onChange(Number.parseInt(newValue));
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { Device } from '@components/PageComponents/Config/Device/index.tsx';
|
||||||
|
import { useDevice } from '@core/stores/deviceStore.ts';
|
||||||
|
import { useUnsafeRolesDialog } from '@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@core/stores/deviceStore', () => ({
|
||||||
|
useDevice: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog', () => ({
|
||||||
|
useUnsafeRolesDialog: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Device component with UnsafeRolesDialog', () => {
|
||||||
|
const setWorkingConfigMock = vi.fn();
|
||||||
|
const validateRoleDialogResultMock = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
|
||||||
|
// Mock useDevice hook
|
||||||
|
(useDevice as any).mockReturnValue({
|
||||||
|
config: {
|
||||||
|
device: {}
|
||||||
|
},
|
||||||
|
setWorkingConfig: setWorkingConfigMock
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock useUnsafeRolesDialog hook
|
||||||
|
(useUnsafeRolesDialog as any).mockReturnValue({
|
||||||
|
validateRoleDialogResult: validateRoleDialogResultMock
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the validateRoleDialogResult from the hook', () => {
|
||||||
|
render(<Device />);
|
||||||
|
|
||||||
|
// Verify the hook was called
|
||||||
|
expect(useUnsafeRolesDialog).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify the form is using the validation function from the hook
|
||||||
|
expect(setWorkingConfigMock).not.toHaveBeenCalled(); // Just ensure the component rendered without errors
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,42 +3,22 @@ import { create } from "@bufbuild/protobuf";
|
|||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useUnsafeRoles } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRoles.ts";
|
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||||
|
|
||||||
export const Device = () => {
|
export const Device = () => {
|
||||||
|
const { config, setWorkingConfig } = useDevice();
|
||||||
const { config, setWorkingConfig, setDialogOpen } = useDevice();
|
const { validateRoleSelection } = useUnsafeRolesDialog();
|
||||||
const { agreedToUnSafeRoles } = useUnsafeRoles();
|
|
||||||
|
|
||||||
const onSubmit = (data: DeviceValidation) => {
|
const onSubmit = (data: DeviceValidation) => {
|
||||||
setWorkingConfig(
|
setWorkingConfig(
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
create(Protobuf.Config.ConfigSchema, {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
case: "device",
|
case: "device",
|
||||||
value: data
|
value: data,
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// deno-lint-ignore require-await
|
|
||||||
async function handleOnBeforeChange(newValue: string) {
|
|
||||||
if (newValue === "ROUTER" || newValue === 'REPEATER') {
|
|
||||||
// Open the dialog to confirm the user wants to select an unsafe role
|
|
||||||
setDialogOpen('unsafeRoles', true);
|
|
||||||
|
|
||||||
// We checked the persisted value of agreedToUnSafeRoles in localStorage to see if the user has agreed to unsafe roles
|
|
||||||
if (agreedToUnSafeRoles) {
|
|
||||||
return newValue;
|
|
||||||
} else {
|
|
||||||
// If the user has not agreed to unsafe roles, we return false to prevent the role from being set
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicForm<DeviceValidation>
|
<DynamicForm<DeviceValidation>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
@@ -53,7 +33,7 @@ export const Device = () => {
|
|||||||
name: "role",
|
name: "role",
|
||||||
label: "Role",
|
label: "Role",
|
||||||
description: "What role the device performs on the mesh",
|
description: "What role the device performs on the mesh",
|
||||||
onBeforeChange: handleOnBeforeChange,
|
validate: validateRoleSelection,
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: Protobuf.Config.Config_DeviceConfig_Role,
|
enumValue: Protobuf.Config.Config_DeviceConfig_Role,
|
||||||
formatEnumName: true,
|
formatEnumName: true,
|
||||||
|
|||||||
@@ -50,15 +50,28 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DialogClose = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.DialogCloseProps & React.RefAttributes<HTMLButtonElement> & { className?: string }) => (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
name="close"
|
||||||
|
className={cn(
|
||||||
|
"absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
);
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
@@ -119,4 +132,5 @@ export {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
DialogClose,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function ErrorPage({ error }: { error: Error }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="w-full h-full overflow-y-auto dark:bg-background-primary dark:text-text-primary">
|
<article className="w-full h-screen overflow-y-auto dark:bg-background-primary dark:text-text-primary">
|
||||||
<section className="flex shrink md:flex-row gap-16 mt-20 px-4 md:px-8 text-lg md:text-xl space-y-2 place-items-center dark:bg-background-primary text-slate-900 dark:text-text-primary">
|
<section className="flex shrink md:flex-row gap-16 mt-20 px-4 md:px-8 text-lg md:text-xl space-y-2 place-items-center dark:bg-background-primary text-slate-900 dark:text-text-primary">
|
||||||
<div>
|
<div>
|
||||||
<Heading as="h2" className="text-text-primary">
|
<Heading as="h2" className="text-text-primary">
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export interface Device {
|
|||||||
state: MessageState,
|
state: MessageState,
|
||||||
) => void;
|
) => void;
|
||||||
setDialogOpen: (dialog: DialogVariant, open: boolean) => void;
|
setDialogOpen: (dialog: DialogVariant, open: boolean) => void;
|
||||||
|
getDialogOpen: (dialog: DialogVariant) => boolean;
|
||||||
processPacket: (data: ProcessPacketParams) => void;
|
processPacket: (data: ProcessPacketParams) => void;
|
||||||
setMessageDraft: (message: string) => void;
|
setMessageDraft: (message: string) => void;
|
||||||
}
|
}
|
||||||
@@ -597,6 +598,13 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
getDialogOpen: (dialog: DialogVariant) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error("Device not found");
|
||||||
|
}
|
||||||
|
return device.dialog[dialog];
|
||||||
|
},
|
||||||
processPacket(data: ProcessPacketParams) {
|
processPacket(data: ProcessPacketParams) {
|
||||||
set(
|
set(
|
||||||
produce<DeviceState>((draft) => {
|
produce<DeviceState>((draft) => {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { eventBus } from "@core/utils/eventBus.ts";
|
||||||
|
|
||||||
|
describe("EventBus", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset event listeners before each test
|
||||||
|
(eventBus as any).listeners = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should register an event listener and trigger it on emit", () => {
|
||||||
|
const mockCallback = vi.fn();
|
||||||
|
|
||||||
|
eventBus.on("dialog:unsafeRoles", mockCallback);
|
||||||
|
eventBus.emit("dialog:unsafeRoles", { action: "confirm" });
|
||||||
|
|
||||||
|
expect(mockCallback).toHaveBeenCalledWith({ action: "confirm" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove an event listener with off", () => {
|
||||||
|
const mockCallback = vi.fn();
|
||||||
|
|
||||||
|
eventBus.on("dialog:unsafeRoles", mockCallback);
|
||||||
|
eventBus.off("dialog:unsafeRoles", mockCallback);
|
||||||
|
eventBus.emit("dialog:unsafeRoles", { action: "dismiss" });
|
||||||
|
|
||||||
|
expect(mockCallback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an unsubscribe function from on", () => {
|
||||||
|
const mockCallback = vi.fn();
|
||||||
|
const unsubscribe = eventBus.on("dialog:unsafeRoles", mockCallback);
|
||||||
|
|
||||||
|
unsubscribe();
|
||||||
|
eventBus.emit("dialog:unsafeRoles", { action: "confirm" });
|
||||||
|
|
||||||
|
expect(mockCallback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow multiple listeners for the same event", () => {
|
||||||
|
const mockCallback1 = vi.fn();
|
||||||
|
const mockCallback2 = vi.fn();
|
||||||
|
|
||||||
|
eventBus.on("dialog:unsafeRoles", mockCallback1);
|
||||||
|
eventBus.on("dialog:unsafeRoles", mockCallback2);
|
||||||
|
eventBus.emit("dialog:unsafeRoles", { action: "confirm" });
|
||||||
|
|
||||||
|
expect(mockCallback1).toHaveBeenCalledWith({ action: "confirm" });
|
||||||
|
expect(mockCallback2).toHaveBeenCalledWith({ action: "confirm" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only remove the specific listener when off is called", () => {
|
||||||
|
const mockCallback1 = vi.fn();
|
||||||
|
const mockCallback2 = vi.fn();
|
||||||
|
|
||||||
|
eventBus.on("dialog:unsafeRoles", mockCallback1);
|
||||||
|
eventBus.on("dialog:unsafeRoles", mockCallback2);
|
||||||
|
eventBus.off("dialog:unsafeRoles", mockCallback1);
|
||||||
|
eventBus.emit("dialog:unsafeRoles", { action: "dismiss" });
|
||||||
|
|
||||||
|
expect(mockCallback1).not.toHaveBeenCalled();
|
||||||
|
expect(mockCallback2).toHaveBeenCalledWith({ action: "dismiss" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail when calling off on a non-existent listener", () => {
|
||||||
|
const mockCallback = vi.fn();
|
||||||
|
eventBus.off("dialog:unsafeRoles", mockCallback);
|
||||||
|
eventBus.emit("dialog:unsafeRoles", { action: "confirm" });
|
||||||
|
|
||||||
|
expect(mockCallback).not.toHaveBeenCalled(); // No error should occur
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export type EventMap = {
|
||||||
|
'dialog:unsafeRoles': {
|
||||||
|
action: 'confirm' | 'dismiss';
|
||||||
|
};
|
||||||
|
// add more events as required
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventName = keyof EventMap;
|
||||||
|
export type EventCallback<T extends EventName> = (data: EventMap[T]) => void;
|
||||||
|
|
||||||
|
class EventBus {
|
||||||
|
private listeners: { [K in EventName]?: Array<EventCallback<K>> } = {};
|
||||||
|
|
||||||
|
public on<T extends EventName>(event: T, callback: EventCallback<T>): () => void {
|
||||||
|
if (!this.listeners[event]) {
|
||||||
|
this.listeners[event] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners[event]?.push(callback as any);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off(event, callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public off<T extends EventName>(event: T, callback: EventCallback<T>): void {
|
||||||
|
if (!this.listeners[event]) return;
|
||||||
|
|
||||||
|
const callbackIndex = this.listeners[event]?.indexOf(callback as any);
|
||||||
|
if (callbackIndex !== undefined && callbackIndex > -1) {
|
||||||
|
this.listeners[event]?.splice(callbackIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit<T extends EventName>(event: T, data: EventMap[T]): void {
|
||||||
|
if (!this.listeners[event]) return;
|
||||||
|
|
||||||
|
this.listeners[event]?.forEach(callback => {
|
||||||
|
callback(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventBus = new EventBus();
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { vi } from 'vitest';
|
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
// Enable auto mocks for our UI components
|
// Enable auto mocks for our UI components
|
||||||
|
|||||||
Reference in New Issue
Block a user