15 changed files with 460 additions and 274 deletions
@ -1,88 +1,91 @@ |
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'; |
|||
import { render, screen, fireEvent } from '@testing-library/react'; |
|||
import { UnsafeRolesDialog } from '@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx'; |
|||
import { useUnsafeRoles } from '@components/Dialog/UnsafeRolesDialog/useUnsafeRoles.ts'; |
|||
|
|||
vi.mock('@components/Dialog/UnsafeRolesDialog/useUnsafeRoles', () => ({ |
|||
useUnsafeRoles: vi.fn() |
|||
})); |
|||
|
|||
describe('UnsafeRolesDialog', () => { |
|||
const getConfirmStateMock = vi.fn(); |
|||
const toggleConfirmStateMock = vi.fn(); |
|||
const handleCloseDialogMock = vi.fn(); |
|||
const onOpenChangeMock = vi.fn(); |
|||
|
|||
beforeEach(() => { |
|||
vi.resetAllMocks(); |
|||
|
|||
getConfirmStateMock.mockReturnValue(false); |
|||
|
|||
(useUnsafeRoles as any).mockReturnValue({ |
|||
getConfirmState: getConfirmStateMock, |
|||
toggleConfirmState: toggleConfirmStateMock, |
|||
handleCloseDialog: handleCloseDialogMock |
|||
}); |
|||
// deno-lint-ignore-file
|
|||
import { render, screen, fireEvent } from "@testing-library/react"; |
|||
import { describe, it, expect, vi } from "vitest"; |
|||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx"; |
|||
import { eventBus } from "@core/utils/eventBus.ts"; |
|||
import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; |
|||
|
|||
describe("UnsafeRolesDialog", () => { |
|||
const mockDevice = { |
|||
setDialogOpen: vi.fn(), |
|||
}; |
|||
|
|||
const renderWithDeviceContext = (ui: any) => { |
|||
return render( |
|||
<DeviceWrapper device={mockDevice}> |
|||
{ui} |
|||
</DeviceWrapper> |
|||
); |
|||
}; |
|||
|
|||
it("renders the dialog when open is true", () => { |
|||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />); |
|||
|
|||
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(); |
|||
|
|||
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', () => { |
|||
render(<UnsafeRolesDialog open={false} onOpenChange={onOpenChangeMock} />); |
|||
it("displays the correct links", () => { |
|||
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 }); |
|||
|
|||
it('should render when open is true', () => { |
|||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
|
|||
expect(screen.getByRole('dialog')).toBeInTheDocument(); |
|||
expect(screen.getByRole('heading')).toBeInTheDocument(); |
|||
expect(screen.getByText('Are you sure?')).toBeInTheDocument(); |
|||
expect(screen.getAllByRole('link')).length(2); |
|||
expect(screen.getByRole('checkbox')).toBeInTheDocument(); |
|||
expect(screen.getByText('Yes, I know what I\'m doing')).toBeInTheDocument(); |
|||
expect(screen.getByRole('button', { name: /dismiss/i })).toBeInTheDocument(); |
|||
expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument(); |
|||
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 have disabled confirm button when checkbox is unchecked', () => { |
|||
getConfirmStateMock.mockReturnValue(false); |
|||
|
|||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
it("does not allow confirmation until checkbox is checked", () => { |
|||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />); |
|||
|
|||
expect(screen.getByRole('button', { name: /confirm/i })).toBeDisabled(); |
|||
}); |
|||
const confirmButton = screen.getByRole("button", { name: /confirm/i }); |
|||
|
|||
it('should have enabled confirm button when checkbox is checked', () => { |
|||
getConfirmStateMock.mockReturnValue(true); |
|||
expect(confirmButton).toBeDisabled(); |
|||
|
|||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
const checkbox = screen.getByRole("checkbox"); |
|||
fireEvent.click(checkbox); |
|||
|
|||
expect(screen.getByRole('button', { name: /confirm/i })).not.toBeDisabled(); |
|||
expect(confirmButton).toBeEnabled(); |
|||
}); |
|||
|
|||
it('should call toggleConfirmState when checkbox is clicked', () => { |
|||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
it("emits the correct event when closing via close button", () => { |
|||
const eventSpy = vi.spyOn(eventBus, "emit"); |
|||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />); |
|||
|
|||
fireEvent.click(screen.getByRole('checkbox')); |
|||
const dismissButton = screen.getByRole("button", { name: /close/i }); |
|||
fireEvent.click(dismissButton); |
|||
|
|||
expect(toggleConfirmStateMock).toHaveBeenCalledTimes(1); |
|||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "dismiss" }); |
|||
}); |
|||
|
|||
it('should call handleCloseDialog with "dismiss" when dismiss button is clicked', () => { |
|||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
it("emits the correct event when dismissing", () => { |
|||
const eventSpy = vi.spyOn(eventBus, "emit"); |
|||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />); |
|||
|
|||
fireEvent.click(screen.getByRole('button', { name: /dismiss/i })); |
|||
const dismissButton = screen.getByRole("button", { name: /dismiss/i }); |
|||
fireEvent.click(dismissButton); |
|||
|
|||
expect(handleCloseDialogMock).toHaveBeenCalledWith('dismiss'); |
|||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "dismiss" }); |
|||
}); |
|||
|
|||
it('should call handleCloseDialog with "confirm" when confirm button is clicked', () => { |
|||
getConfirmStateMock.mockReturnValue(true); |
|||
it("emits the correct event when confirming", () => { |
|||
const eventSpy = vi.spyOn(eventBus, "emit"); |
|||
renderWithDeviceContext(<UnsafeRolesDialog open={true} onOpenChange={vi.fn()} />); |
|||
|
|||
render(<UnsafeRolesDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
const checkbox = screen.getByRole("checkbox"); |
|||
const confirmButton = screen.getByRole("button", { name: /confirm/i }); |
|||
|
|||
fireEvent.click(screen.getByRole('button', { name: /confirm/i })); |
|||
fireEvent.click(checkbox); |
|||
fireEvent.click(confirmButton); |
|||
|
|||
expect(handleCloseDialogMock).toHaveBeenCalledWith("confirm"); |
|||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { action: "confirm" }); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
@ -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, |
|||
}; |
|||
}; |
|||
@ -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
|
|||
}); |
|||
}); |
|||
@ -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(); |
|||
Loading…
Reference in new issue