Browse Source
* fixed tsc errors * fixed tsc errors * fixed tsc errors * fixing tsc errors * fixing more tsc errors * fixing more tsc errors * fixed tsc errors * fixing tsc errors * fixing PR issues * commented out tsc check * completing tsc fixes * updating lockfile * removed react-hookspull/650/head
committed by
GitHub
59 changed files with 1140 additions and 2799 deletions
File diff suppressed because it is too large
@ -13,6 +13,7 @@ |
|||||
"dev": "deno task dev:ui", |
"dev": "deno task dev:ui", |
||||
"dev:ui": "VITE_APP_VERSION=development deno run -A npm:vite dev", |
"dev:ui": "VITE_APP_VERSION=development deno run -A npm:vite dev", |
||||
"test": "deno run -A npm:vitest", |
"test": "deno run -A npm:vitest", |
||||
|
"check": "deno check", |
||||
"preview": "deno run -A npm:vite preview", |
"preview": "deno run -A npm:vite preview", |
||||
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ." |
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ." |
||||
}, |
}, |
||||
@ -35,10 +36,11 @@ |
|||||
"homepage": "https://meshtastic.org", |
"homepage": "https://meshtastic.org", |
||||
"dependencies": { |
"dependencies": { |
||||
"@bufbuild/protobuf": "^2.2.5", |
"@bufbuild/protobuf": "^2.2.5", |
||||
"@meshtastic/core": "npm:@jsr/[email protected]", |
"@meshtastic/core": "npm:@jsr/[email protected]", |
||||
"@meshtastic/transport-http": "npm:@jsr/[email protected]", |
"@meshtastic/js": "npm:@jsr/[email protected]", |
||||
"@meshtastic/transport-web-bluetooth": "npm:@jsr/[email protected]", |
"@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http", |
||||
"@meshtastic/transport-web-serial": "npm:@jsr/[email protected]", |
"@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth", |
||||
|
"@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial", |
||||
"@noble/curves": "^1.9.0", |
"@noble/curves": "^1.9.0", |
||||
"@radix-ui/react-accordion": "^1.2.8", |
"@radix-ui/react-accordion": "^1.2.8", |
||||
"@radix-ui/react-checkbox": "^1.2.3", |
"@radix-ui/react-checkbox": "^1.2.3", |
||||
@ -60,6 +62,7 @@ |
|||||
"@tanstack/react-router-devtools": "^1.120.16", |
"@tanstack/react-router-devtools": "^1.120.16", |
||||
"@tanstack/router-devtools": "^1.120.15", |
"@tanstack/router-devtools": "^1.120.15", |
||||
"@turf/turf": "^7.2.0", |
"@turf/turf": "^7.2.0", |
||||
|
"@types/web-bluetooth": "^0.0.21", |
||||
"base64-js": "^1.5.1", |
"base64-js": "^1.5.1", |
||||
"class-variance-authority": "^0.7.1", |
"class-variance-authority": "^0.7.1", |
||||
"clsx": "^2.1.1", |
"clsx": "^2.1.1", |
||||
@ -81,10 +84,8 @@ |
|||||
"react-map-gl": "8.0.4", |
"react-map-gl": "8.0.4", |
||||
"react-qrcode-logo": "^3.0.0", |
"react-qrcode-logo": "^3.0.0", |
||||
"rfc4648": "^1.5.4", |
"rfc4648": "^1.5.4", |
||||
"vite-plugin-i18n-ally": "^6.0.1", |
"zod": "^3.25.62", |
||||
"vite-plugin-node-polyfills": "^0.23.0", |
"zustand": "5.0.5" |
||||
"zod": "^3.25.0", |
|
||||
"zustand": "5.0.4" |
|
||||
}, |
}, |
||||
"devDependencies": { |
"devDependencies": { |
||||
"@tailwindcss/postcss": "^4.1.5", |
"@tailwindcss/postcss": "^4.1.5", |
||||
@ -93,13 +94,12 @@ |
|||||
"@testing-library/react": "^16.3.0", |
"@testing-library/react": "^16.3.0", |
||||
"@testing-library/user-event": "^14.6.1", |
"@testing-library/user-event": "^14.6.1", |
||||
"@types/chrome": "^0.0.318", |
"@types/chrome": "^0.0.318", |
||||
"@types/js-cookie": "^3.0.6", |
|
||||
"@types/node": "^22.15.3", |
"@types/node": "^22.15.3", |
||||
"@types/react": "^19.1.2", |
"@types/react": "^19.1.2", |
||||
"@types/react-dom": "^19.1.3", |
"@types/react-dom": "^19.1.3", |
||||
"@types/serviceworker": "^0.0.133", |
"@types/serviceworker": "^0.0.133", |
||||
|
"@types/js-cookie": "^3.0.6", |
||||
"@types/w3c-web-serial": "^1.0.8", |
"@types/w3c-web-serial": "^1.0.8", |
||||
"@types/web-bluetooth": "^0.0.21", |
|
||||
"@vitejs/plugin-react": "^4.4.1", |
"@vitejs/plugin-react": "^4.4.1", |
||||
"autoprefixer": "^10.4.21", |
"autoprefixer": "^10.4.21", |
||||
"gzipper": "^8.2.1", |
"gzipper": "^8.2.1", |
||||
|
|||||
@ -1,149 +0,0 @@ |
|||||
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"; |
|
||||
import { useAppStore } from "@core/stores/appStore.ts"; |
|
||||
import type { Protobuf } from "@meshtastic/core"; |
|
||||
|
|
||||
vi.mock("@core/stores/deviceStore"); |
|
||||
vi.mock("@core/stores/appStore"); |
|
||||
|
|
||||
const mockUseDevice = vi.mocked(useDevice); |
|
||||
const mockUseAppStore = vi.mocked(useAppStore); |
|
||||
|
|
||||
vi.mock("@tanstack/react-router", () => ({ |
|
||||
useNavigate: vi.fn(), |
|
||||
})); |
|
||||
|
|
||||
describe("NodeDetailsDialog", () => { |
|
||||
const mockNode = { |
|
||||
num: 1234, |
|
||||
user: { |
|
||||
longName: "Test Node", |
|
||||
shortName: "TN", |
|
||||
hwModel: 1, |
|
||||
role: 1, |
|
||||
}, |
|
||||
lastHeard: 1697500000, |
|
||||
position: { |
|
||||
latitudeI: 450000000, |
|
||||
longitudeI: -750000000, |
|
||||
altitude: 200, |
|
||||
}, |
|
||||
deviceMetrics: { |
|
||||
airUtilTx: 50.123, |
|
||||
channelUtilization: 75.456, |
|
||||
batteryLevel: 88.789, |
|
||||
voltage: 4.2, |
|
||||
uptimeSeconds: 3600, |
|
||||
}, |
|
||||
} as unknown as Protobuf.Mesh.NodeInfo; |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
vi.resetAllMocks(); |
|
||||
|
|
||||
mockUseDevice.mockReturnValue({ |
|
||||
getNode: (nodeNum: number) => { |
|
||||
if (nodeNum === 1234) { |
|
||||
return mockNode; |
|
||||
} |
|
||||
return undefined; |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
mockUseAppStore.mockReturnValue({ |
|
||||
nodeNumDetails: 1234, |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("renders node details correctly", () => { |
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />); |
|
||||
|
|
||||
expect(screen.getByText(/Node Details for Test Node \(TN\)/i)) |
|
||||
.toBeInTheDocument(); |
|
||||
|
|
||||
expect(screen.getByText("Node Number: 1234")).toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Node Hex: !/i)).toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Last Heard:/i)).toBeInTheDocument(); |
|
||||
|
|
||||
expect(screen.getByText(/Coordinates:/i)).toBeInTheDocument(); |
|
||||
const link = screen.getByRole("link", { name: /^45, -75$/ }); |
|
||||
|
|
||||
expect(link).toBeInTheDocument(); |
|
||||
expect(link).toHaveAttribute( |
|
||||
"href", |
|
||||
expect.stringContaining("openstreetmap.org"), |
|
||||
); |
|
||||
expect(screen.getByText(/Altitude: 200m/i)).toBeInTheDocument(); |
|
||||
|
|
||||
expect(screen.getByText(/Air TX utilization: 50.12%/i)).toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Channel utilization: 75.46%/i)) |
|
||||
.toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Battery level: 88.79%/i)).toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Voltage: 4.20V/i)).toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Uptime:/i)).toBeInTheDocument(); |
|
||||
|
|
||||
expect(screen.getByText(/All Raw Metrics:/i)).toBeInTheDocument(); |
|
||||
}); |
|
||||
|
|
||||
it("renders null if node is undefined", () => { |
|
||||
const requestedNodeNum = 5678; |
|
||||
|
|
||||
mockUseAppStore.mockReturnValue({ |
|
||||
nodeNumDetails: requestedNodeNum, |
|
||||
}); |
|
||||
|
|
||||
mockUseDevice.mockReturnValue({ |
|
||||
getNode: (nodeNum: number) => { |
|
||||
if (nodeNum === requestedNodeNum) { |
|
||||
return undefined; |
|
||||
} |
|
||||
if (nodeNum === 1234) { |
|
||||
return mockNode; |
|
||||
} |
|
||||
return undefined; |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
const { container } = render( |
|
||||
<NodeDetailsDialog open onOpenChange={() => {}} />, |
|
||||
); |
|
||||
|
|
||||
expect(container.firstChild).toBeNull(); |
|
||||
expect(screen.queryByText(/Node Details for/i)).not.toBeInTheDocument(); |
|
||||
}); |
|
||||
|
|
||||
it("renders correctly when position is missing", () => { |
|
||||
const nodeWithoutPosition = { ...mockNode, position: undefined }; |
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutPosition }); |
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 }); |
|
||||
|
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />); |
|
||||
|
|
||||
expect(screen.queryByText(/Coordinates:/i)).not.toBeInTheDocument(); |
|
||||
expect(screen.queryByText(/Altitude:/i)).not.toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument(); |
|
||||
}); |
|
||||
|
|
||||
it("renders correctly when deviceMetrics are missing", () => { |
|
||||
const nodeWithoutMetrics = { ...mockNode, deviceMetrics: undefined }; |
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutMetrics }); |
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 }); |
|
||||
|
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />); |
|
||||
|
|
||||
expect(screen.queryByText(/Device Metrics:/i)).not.toBeInTheDocument(); |
|
||||
expect(screen.queryByText(/Air TX utilization:/i)).not.toBeInTheDocument(); |
|
||||
expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument(); |
|
||||
}); |
|
||||
|
|
||||
it("renders 'Never' for lastHeard when timestamp is 0", () => { |
|
||||
const nodeNeverHeard = { ...mockNode, lastHeard: 0 }; |
|
||||
mockUseDevice.mockReturnValue({ getNode: () => nodeNeverHeard }); |
|
||||
mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 }); |
|
||||
|
|
||||
render(<NodeDetailsDialog open onOpenChange={() => {}} />); |
|
||||
|
|
||||
expect(screen.getByText(/Last Heard: Never/i)).toBeInTheDocument(); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,86 +0,0 @@ |
|||||
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"; |
|
||||
import { useMessageStore } from "@core/stores/messageStore/index.ts"; |
|
||||
|
|
||||
vi.mock("@core/stores/messageStore", () => ({ |
|
||||
useMessageStore: vi.fn(() => ({ activeChat: "chat-123" })), |
|
||||
})); |
|
||||
vi.mock("@core/stores/deviceStore", () => ({ |
|
||||
useDevice: vi.fn(() => ({ |
|
||||
removeNode: vi.fn(), |
|
||||
setDialogOpen: vi.fn(), |
|
||||
getNodeError: vi.fn(), |
|
||||
clearNodeError: vi.fn(), |
|
||||
})), |
|
||||
})); |
|
||||
|
|
||||
describe("useRefreshKeysDialog Hook", () => { |
|
||||
let removeNodeMock: Mock; |
|
||||
let setDialogOpenMock: Mock; |
|
||||
let getNodeErrorMock: Mock; |
|
||||
let clearNodeErrorMock: Mock; |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
vi.clearAllMocks(); |
|
||||
|
|
||||
removeNodeMock = vi.fn(); |
|
||||
setDialogOpenMock = vi.fn(); |
|
||||
getNodeErrorMock = vi.fn().mockReturnValue(undefined); |
|
||||
clearNodeErrorMock = vi.fn(); |
|
||||
|
|
||||
vi.mocked(useDevice).mockReturnValue({ |
|
||||
removeNode: removeNodeMock, |
|
||||
setDialogOpen: setDialogOpenMock, |
|
||||
getNodeError: getNodeErrorMock, |
|
||||
clearNodeError: clearNodeErrorMock, |
|
||||
}); |
|
||||
|
|
||||
vi.mocked(useMessageStore).mockReturnValue({ |
|
||||
activeChat: "chat-123", |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("handleNodeRemove should remove the node and update dialog if there is an error", () => { |
|
||||
getNodeErrorMock.mockReturnValue({ node: "node-abc" }); |
|
||||
|
|
||||
const { result } = renderHook(() => useRefreshKeysDialog()); |
|
||||
act(() => { |
|
||||
result.current.handleNodeRemove(); |
|
||||
}); |
|
||||
|
|
||||
expect(getNodeErrorMock).toHaveBeenCalledTimes(1); |
|
||||
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123"); |
|
||||
expect(clearNodeErrorMock).toHaveBeenCalledTimes(1); |
|
||||
expect(clearNodeErrorMock).toHaveBeenCalledWith("chat-123"); |
|
||||
expect(removeNodeMock).toHaveBeenCalledTimes(1); |
|
||||
expect(removeNodeMock).toHaveBeenCalledWith("node-abc"); |
|
||||
expect(setDialogOpenMock).toHaveBeenCalledTimes(1); |
|
||||
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false); |
|
||||
}); |
|
||||
|
|
||||
it("handleNodeRemove should do nothing if there is no error", () => { |
|
||||
const { result } = renderHook(() => useRefreshKeysDialog()); |
|
||||
act(() => { |
|
||||
result.current.handleNodeRemove(); |
|
||||
}); |
|
||||
|
|
||||
expect(getNodeErrorMock).toHaveBeenCalledTimes(1); |
|
||||
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123"); |
|
||||
expect(clearNodeErrorMock).not.toHaveBeenCalled(); |
|
||||
expect(removeNodeMock).not.toHaveBeenCalled(); |
|
||||
expect(setDialogOpenMock).not.toHaveBeenCalled(); |
|
||||
}); |
|
||||
|
|
||||
it("handleCloseDialog should close the dialog", () => { |
|
||||
const { result } = renderHook(() => useRefreshKeysDialog()); |
|
||||
|
|
||||
act(() => { |
|
||||
result.current.handleCloseDialog(); |
|
||||
}); |
|
||||
|
|
||||
expect(setDialogOpenMock).toHaveBeenCalledTimes(1); |
|
||||
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,134 +0,0 @@ |
|||||
import { fireEvent, render, screen } from "@testing-library/react"; |
|
||||
import { describe, expect, it, vi } from "vitest"; |
|
||||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx"; |
|
||||
import { |
|
||||
createMemoryHistory, |
|
||||
createRootRoute, |
|
||||
createRouter, |
|
||||
RouterProvider, |
|
||||
} from "@tanstack/react-router"; |
|
||||
import { eventBus } from "@core/utils/eventBus.ts"; |
|
||||
import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; |
|
||||
|
|
||||
const rootRoute = createRootRoute(); |
|
||||
|
|
||||
describe.skip("UnsafeRolesDialog", () => { |
|
||||
const mockDevice = { |
|
||||
setDialogOpen: vi.fn(), |
|
||||
}; |
|
||||
|
|
||||
const renderWithProviders = (ui: React.ReactNode) => { |
|
||||
const testRouter = createRouter({ |
|
||||
routeTree: rootRoute, |
|
||||
history: createMemoryHistory(), |
|
||||
}); |
|
||||
|
|
||||
return render( |
|
||||
<RouterProvider router={testRouter}> |
|
||||
<DeviceWrapper device={mockDevice}> |
|
||||
{ui} |
|
||||
</DeviceWrapper> |
|
||||
</RouterProvider>, |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
it("renders the dialog when open is true", () => { |
|
||||
renderWithProviders( |
|
||||
<UnsafeRolesDialog open 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("displays the correct links", () => { |
|
||||
renderWithProviders( |
|
||||
<UnsafeRolesDialog open 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, |
|
||||
}); |
|
||||
|
|
||||
expect(docLink).toHaveAttribute( |
|
||||
"to", |
|
||||
"https://meshtastic.org/docs/configuration/radio/device/", |
|
||||
); |
|
||||
expect(blogLink).toHaveAttribute( |
|
||||
"to", |
|
||||
"https://meshtastic.org/blog/choosing-the-right-device-role/", |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
it("does not allow confirmation until checkbox is checked", () => { |
|
||||
renderWithProviders( |
|
||||
<UnsafeRolesDialog open onOpenChange={vi.fn()} />, |
|
||||
); |
|
||||
|
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i }); |
|
||||
|
|
||||
expect(confirmButton).toBeDisabled(); |
|
||||
|
|
||||
const checkbox = screen.getByRole("checkbox"); |
|
||||
fireEvent.click(checkbox); |
|
||||
|
|
||||
expect(confirmButton).toBeEnabled(); |
|
||||
}); |
|
||||
|
|
||||
it("emits the correct event when closing via close button", () => { |
|
||||
const eventSpy = vi.spyOn(eventBus, "emit"); |
|
||||
renderWithProviders( |
|
||||
<UnsafeRolesDialog open onOpenChange={vi.fn()} />, |
|
||||
); |
|
||||
|
|
||||
const dismissButton = screen.getByRole("button", { name: /close/i }); |
|
||||
fireEvent.click(dismissButton); |
|
||||
|
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { |
|
||||
action: "dismiss", |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("emits the correct event when dismissing", () => { |
|
||||
const eventSpy = vi.spyOn(eventBus, "emit"); |
|
||||
renderWithProviders( |
|
||||
<UnsafeRolesDialog open onOpenChange={vi.fn()} />, |
|
||||
); |
|
||||
|
|
||||
const dismissButton = screen.getByRole("button", { name: /dismiss/i }); |
|
||||
fireEvent.click(dismissButton); |
|
||||
|
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { |
|
||||
action: "dismiss", |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("emits the correct event when confirming", () => { |
|
||||
const eventSpy = vi.spyOn(eventBus, "emit"); |
|
||||
renderWithProviders( |
|
||||
<UnsafeRolesDialog open onOpenChange={vi.fn()} />, |
|
||||
); |
|
||||
|
|
||||
const checkbox = screen.getByRole("checkbox"); |
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i }); |
|
||||
|
|
||||
fireEvent.click(checkbox); |
|
||||
fireEvent.click(confirmButton); |
|
||||
|
|
||||
expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { |
|
||||
action: "confirm", |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,314 +0,0 @@ |
|||||
import { describe, expect, it, vi } from "vitest"; |
|
||||
import { fireEvent, render, screen, waitFor } from "@core/utils/test.tsx"; |
|
||||
import { DynamicForm } from "./DynamicForm.tsx"; |
|
||||
import { z } from "zod/v4"; |
|
||||
import { useAppStore } from "@core/stores/appStore.ts"; |
|
||||
import userEvent from "@testing-library/user-event"; |
|
||||
|
|
||||
vi.mock("react-i18next", () => ({ |
|
||||
useTranslation: () => ({ |
|
||||
t: (key: string | string[]) => (Array.isArray(key) ? key[0] : key), |
|
||||
}), |
|
||||
})); |
|
||||
|
|
||||
const addErrorMock = vi.fn(); |
|
||||
const removeErrorMock = vi.fn(); |
|
||||
|
|
||||
vi.mock("@core/stores/appStore.ts", () => ({ |
|
||||
useAppStore: () => ({ |
|
||||
addError: addErrorMock, |
|
||||
removeError: removeErrorMock, |
|
||||
}), |
|
||||
})); |
|
||||
|
|
||||
describe.skip("DynamicForm", () => { |
|
||||
const schema = z.object({ |
|
||||
name: z.string().min(3, { message: "Too short" }), |
|
||||
}); |
|
||||
|
|
||||
const fieldGroups = [ |
|
||||
{ |
|
||||
label: "Test Group", |
|
||||
description: "Testing validation", |
|
||||
fields: [ |
|
||||
{ |
|
||||
type: "text", |
|
||||
id: "name", |
|
||||
name: "name", |
|
||||
label: "Name", |
|
||||
description: "Enter your name", |
|
||||
properties: {}, |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
it("shows validation error when input is too short", async () => { |
|
||||
render( |
|
||||
<DynamicForm<z.infer<typeof schema>> |
|
||||
onSubmit={vi.fn()} |
|
||||
validationSchema={schema} |
|
||||
defaultValues={{ name: "" }} |
|
||||
fieldGroups={fieldGroups} |
|
||||
/>, |
|
||||
); |
|
||||
const input = screen.getByLabelText("Name") as HTMLInputElement; |
|
||||
|
|
||||
fireEvent.input(input, { target: { value: "ab" } }); |
|
||||
|
|
||||
const error = await screen.findByText( |
|
||||
"formValidation.tooSmall.string", |
|
||||
); |
|
||||
expect(error).toBeVisible(); |
|
||||
}); |
|
||||
|
|
||||
it("clears validation error when input becomes valid", async () => { |
|
||||
render( |
|
||||
<DynamicForm<z.infer<typeof schema>> |
|
||||
onSubmit={vi.fn()} |
|
||||
validationSchema={schema} |
|
||||
defaultValues={{ name: "" }} |
|
||||
fieldGroups={fieldGroups} |
|
||||
/>, |
|
||||
); |
|
||||
const input = screen.getByLabelText("Name") as HTMLInputElement; |
|
||||
|
|
||||
fireEvent.input(input, { target: { value: "ab" } }); |
|
||||
expect( |
|
||||
await screen.findByText("formValidation.tooSmall.string"), |
|
||||
).toBeVisible(); |
|
||||
|
|
||||
fireEvent.input(input, { target: { value: "abcd" } }); |
|
||||
await waitFor(() => |
|
||||
expect( |
|
||||
screen.queryByText("formValidation.tooSmall.string"), |
|
||||
).toBeNull() |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
it("calls onSubmit when form is valid onChange", async () => { |
|
||||
const onSubmit = vi.fn(); |
|
||||
render( |
|
||||
<DynamicForm<z.infer<typeof schema>> |
|
||||
onSubmit={onSubmit} |
|
||||
validationSchema={schema} |
|
||||
defaultValues={{ name: "" }} |
|
||||
fieldGroups={fieldGroups} |
|
||||
/>, |
|
||||
); |
|
||||
|
|
||||
const input = screen.getByLabelText("Name") as HTMLInputElement; |
|
||||
|
|
||||
fireEvent.input(input, { target: { value: "ab" } }); |
|
||||
expect( |
|
||||
await screen.findByText("formValidation.tooSmall.string"), |
|
||||
).toBeVisible(); |
|
||||
|
|
||||
fireEvent.input(input, { target: { value: "abcd" } }); |
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1); |
|
||||
}); |
|
||||
|
|
||||
expect(onSubmit).toHaveBeenCalledWith( |
|
||||
{ name: "abcd" }, |
|
||||
expect.any(Object), |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
it("renders a button and only calls onSubmit on click with submitType='onSubmit'", async () => { |
|
||||
// Use the userEvent setup
|
|
||||
const user = userEvent.setup(); |
|
||||
const onSubmit = vi.fn(); |
|
||||
|
|
||||
render( |
|
||||
<DynamicForm<z.infer<typeof schema>> |
|
||||
onSubmit={onSubmit} |
|
||||
submitType="onSubmit" |
|
||||
hasSubmitButton |
|
||||
validationSchema={schema} |
|
||||
defaultValues={{ name: "" }} |
|
||||
fieldGroups={fieldGroups} |
|
||||
/>, |
|
||||
); |
|
||||
|
|
||||
const nameInput = screen.getByLabelText("Name"); |
|
||||
const submitButton = screen.getByRole("button", { name: /submit/i }); |
|
||||
|
|
||||
expect(submitButton).toBeInTheDocument(); |
|
||||
await user.type(nameInput, "ab"); |
|
||||
|
|
||||
expect(await screen.findByText("formValidation.tooSmall.string")) |
|
||||
.toBeInTheDocument(); |
|
||||
await user.click(submitButton); |
|
||||
expect(onSubmit).not.toHaveBeenCalled(); |
|
||||
|
|
||||
await user.clear(nameInput); |
|
||||
await user.type(nameInput, "abcd"); |
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(screen.queryByText("formValidation.tooSmall.string")).not |
|
||||
.toBeInTheDocument(); |
|
||||
}); |
|
||||
await user.click(submitButton); |
|
||||
await waitFor(() => { |
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1); |
|
||||
}); |
|
||||
|
|
||||
expect(onSubmit).toHaveBeenCalledWith({ name: "abcd" }, expect.any(Object)); |
|
||||
}); |
|
||||
|
|
||||
it("renders defaultValues correctly", () => { |
|
||||
render( |
|
||||
<DynamicForm<{ name: string }> |
|
||||
onSubmit={vi.fn()} |
|
||||
// no validationSchema
|
|
||||
defaultValues={{ name: "Alice" }} |
|
||||
fieldGroups={[ |
|
||||
{ |
|
||||
label: "Group", |
|
||||
description: "", |
|
||||
fields: [ |
|
||||
{ |
|
||||
type: "text", |
|
||||
name: "name", |
|
||||
label: "Name", |
|
||||
description: "", |
|
||||
properties: {}, |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
]} |
|
||||
/>, |
|
||||
); |
|
||||
const input = screen.getByLabelText("Name") as HTMLInputElement; |
|
||||
expect(input.value).toBe("Alice"); |
|
||||
}); |
|
||||
|
|
||||
it("toggles disabled state based on disabledBy rules", async () => { |
|
||||
const schema = z.object({ |
|
||||
enable: z.boolean(), |
|
||||
follow: z.string(), |
|
||||
}); |
|
||||
render( |
|
||||
<DynamicForm<z.infer<typeof schema>> |
|
||||
onSubmit={vi.fn()} |
|
||||
validationSchema={schema} |
|
||||
defaultValues={{ enable: false, follow: "" }} |
|
||||
fieldGroups={[ |
|
||||
{ |
|
||||
label: "Group", |
|
||||
description: "", |
|
||||
fields: [ |
|
||||
{ |
|
||||
type: "toggle", |
|
||||
name: "enable", |
|
||||
label: "enable", |
|
||||
description: "", |
|
||||
}, |
|
||||
{ |
|
||||
type: "text", |
|
||||
name: "follow", |
|
||||
label: "follow", |
|
||||
description: "", |
|
||||
disabledBy: [{ fieldName: "enable" }], |
|
||||
properties: {}, |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
]} |
|
||||
/>, |
|
||||
); |
|
||||
const enable = screen.getByRole("switch", { |
|
||||
name: "enable", |
|
||||
}) as HTMLInputElement; |
|
||||
|
|
||||
const follow = screen.getByLabelText("follow") as HTMLInputElement; |
|
||||
await waitFor(() => { |
|
||||
expect(enable.getAttribute("aria-checked")).toBe("false"); |
|
||||
expect(follow).toBeDisabled(); |
|
||||
}); |
|
||||
|
|
||||
fireEvent.click(enable); |
|
||||
await waitFor(() => { |
|
||||
expect(enable.getAttribute("aria-checked")).toBe("true"); |
|
||||
expect(follow).not.toBeDisabled(); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("always calls onSubmit onChange when no validationSchema is provided", async () => { |
|
||||
const onSubmit = vi.fn(); |
|
||||
render( |
|
||||
<DynamicForm<{ foo: string }> |
|
||||
onSubmit={onSubmit} |
|
||||
// no validationSchema
|
|
||||
defaultValues={{ foo: "" }} |
|
||||
fieldGroups={[ |
|
||||
{ |
|
||||
label: "G", |
|
||||
description: "", |
|
||||
fields: [ |
|
||||
{ |
|
||||
type: "text", |
|
||||
name: "foo", |
|
||||
label: "Foo", |
|
||||
description: "", |
|
||||
properties: {}, |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
]} |
|
||||
/>, |
|
||||
); |
|
||||
const input = screen.getByLabelText("Foo") as HTMLInputElement; |
|
||||
fireEvent.input(input, { target: { value: "bar" } }); |
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1); |
|
||||
expect(onSubmit).toHaveBeenCalledWith({ foo: "bar" }, expect.any(Object)); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("syncs errors to appStore when formId is set", async () => { |
|
||||
const { addError, removeError } = useAppStore(); |
|
||||
const schema = z.object({ foo: z.string().min(2) }); |
|
||||
const groups = [ |
|
||||
{ |
|
||||
label: "G", |
|
||||
description: "", |
|
||||
fields: [ |
|
||||
{ |
|
||||
type: "text", |
|
||||
name: "foo", |
|
||||
label: "Foo", |
|
||||
description: "", |
|
||||
properties: {}, |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
render( |
|
||||
<DynamicForm<z.infer<typeof schema>> |
|
||||
onSubmit={vi.fn()} |
|
||||
formId="myForm" |
|
||||
validationSchema={schema} |
|
||||
defaultValues={{ foo: "" }} |
|
||||
fieldGroups={groups} |
|
||||
/>, |
|
||||
); |
|
||||
const input = screen.getByLabelText("Foo") as HTMLInputElement; |
|
||||
|
|
||||
fireEvent.input(input, { target: { value: "a" } }); |
|
||||
await screen.findByText(/tooSmall/i); |
|
||||
|
|
||||
expect(addError).toHaveBeenCalledWith("foo", ""); |
|
||||
expect(addError).toHaveBeenCalledWith("myForm", ""); |
|
||||
|
|
||||
fireEvent.input(input, { target: { value: "abc" } }); |
|
||||
await waitFor(() => { |
|
||||
expect(removeError).toHaveBeenCalledWith("foo"); |
|
||||
expect(removeError).toHaveBeenCalledWith("myForm"); |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,131 +0,0 @@ |
|||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
|
||||
import { fireEvent, render, screen, waitFor } 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"; |
|
||||
import { Protobuf } from "@meshtastic/core"; |
|
||||
|
|
||||
vi.mock("@core/stores/deviceStore.ts", () => ({ |
|
||||
useDevice: vi.fn(), |
|
||||
})); |
|
||||
|
|
||||
vi.mock("@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts", () => ({ |
|
||||
useUnsafeRolesDialog: vi.fn(), |
|
||||
})); |
|
||||
|
|
||||
// Mock the DynamicForm component since we're testing the Device component,
|
|
||||
// not the DynamicForm implementation
|
|
||||
vi.mock("@components/Form/DynamicForm", () => ({ |
|
||||
DynamicForm: vi.fn(({ onSubmit }) => { |
|
||||
// Render a simplified version of the form for testing
|
|
||||
return ( |
|
||||
<div data-testid="dynamic-form"> |
|
||||
<select |
|
||||
data-testid="role-select" |
|
||||
onChange={(e) => { |
|
||||
// Simulate the validation and submission process
|
|
||||
const mockData = { role: e.target.value }; |
|
||||
onSubmit(mockData); |
|
||||
}} |
|
||||
> |
|
||||
{Object.entries(Protobuf.Config.Config_DeviceConfig_Role).map(( |
|
||||
[key, value], |
|
||||
) => ( |
|
||||
<option key={key} value={value}> |
|
||||
{key} |
|
||||
</option> |
|
||||
))} |
|
||||
</select> |
|
||||
<button |
|
||||
type="submit" |
|
||||
data-testid="submit-button" |
|
||||
onClick={() => onSubmit({ role: "CLIENT" })} |
|
||||
> |
|
||||
Submit |
|
||||
</button> |
|
||||
</div> |
|
||||
); |
|
||||
}), |
|
||||
})); |
|
||||
|
|
||||
describe("Device component", () => { |
|
||||
const setWorkingConfigMock = vi.fn(); |
|
||||
const validateRoleSelectionMock = vi.fn(); |
|
||||
const mockDeviceConfig = { |
|
||||
role: "CLIENT", |
|
||||
buttonGpio: 0, |
|
||||
buzzerGpio: 0, |
|
||||
rebroadcastMode: "ALL", |
|
||||
nodeInfoBroadcastSecs: 300, |
|
||||
doubleTapAsButtonPress: false, |
|
||||
disableTripleClick: false, |
|
||||
ledHeartbeatDisabled: false, |
|
||||
}; |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
vi.resetAllMocks(); |
|
||||
|
|
||||
// Mock the useDevice hook
|
|
||||
useDevice.mockReturnValue({ |
|
||||
config: { |
|
||||
device: mockDeviceConfig, |
|
||||
}, |
|
||||
setWorkingConfig: setWorkingConfigMock, |
|
||||
}); |
|
||||
|
|
||||
// Mock the useUnsafeRolesDialog hook
|
|
||||
validateRoleSelectionMock.mockResolvedValue(true); |
|
||||
useUnsafeRolesDialog.mockReturnValue({ |
|
||||
validateRoleSelection: validateRoleSelectionMock, |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
afterEach(() => { |
|
||||
vi.clearAllMocks(); |
|
||||
}); |
|
||||
|
|
||||
it("should render the Device form", () => { |
|
||||
render(<Device />); |
|
||||
expect(screen.getByTestId("dynamic-form")).toBeInTheDocument(); |
|
||||
}); |
|
||||
|
|
||||
it("should use the validateRoleSelection from the unsafe roles hook", () => { |
|
||||
render(<Device />); |
|
||||
expect(useUnsafeRolesDialog).toHaveBeenCalled(); |
|
||||
}); |
|
||||
|
|
||||
it("should call setWorkingConfig when form is submitted", async () => { |
|
||||
render(<Device />); |
|
||||
|
|
||||
fireEvent.click(screen.getByTestId("submit-button")); |
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(setWorkingConfigMock).toHaveBeenCalledWith( |
|
||||
expect.objectContaining({ |
|
||||
payloadVariant: { |
|
||||
case: "device", |
|
||||
value: expect.objectContaining({ role: "CLIENT" }), |
|
||||
}, |
|
||||
}), |
|
||||
); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("should create config with proper structure", async () => { |
|
||||
render(<Device />); |
|
||||
|
|
||||
// Simulate form submission
|
|
||||
fireEvent.click(screen.getByTestId("submit-button")); |
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(setWorkingConfigMock).toHaveBeenCalledWith( |
|
||||
expect.objectContaining({ |
|
||||
payloadVariant: { |
|
||||
case: "device", |
|
||||
value: expect.any(Object), |
|
||||
}, |
|
||||
}), |
|
||||
); |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,173 +0,0 @@ |
|||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; |
|
||||
import { Network } from "@components/PageComponents/Config/Network/index.tsx"; |
|
||||
import { useDevice } from "@core/stores/deviceStore.ts"; |
|
||||
import { Protobuf } from "@meshtastic/core"; |
|
||||
|
|
||||
vi.mock("@core/stores/deviceStore", () => ({ |
|
||||
useDevice: vi.fn(), |
|
||||
})); |
|
||||
|
|
||||
vi.mock("@components/Form/DynamicForm", async () => { |
|
||||
const React = await import("react"); |
|
||||
const { useState } = React; |
|
||||
|
|
||||
return { |
|
||||
DynamicForm: ({ onSubmit, defaultValues }) => { |
|
||||
const [wifiEnabled, setWifiEnabled] = useState( |
|
||||
defaultValues.wifiEnabled ?? false, |
|
||||
); |
|
||||
const [ssid, setSsid] = useState(defaultValues.wifiSsid ?? ""); |
|
||||
const [psk, setPsk] = useState(defaultValues.wifiPsk ?? ""); |
|
||||
|
|
||||
return ( |
|
||||
<form |
|
||||
onSubmit={(e) => { |
|
||||
e.preventDefault(); |
|
||||
onSubmit({ |
|
||||
...defaultValues, |
|
||||
wifiEnabled, |
|
||||
wifiSsid: ssid, |
|
||||
wifiPsk: psk, |
|
||||
}); |
|
||||
}} |
|
||||
data-testid="dynamic-form" |
|
||||
> |
|
||||
<input |
|
||||
type="checkbox" |
|
||||
aria-label="WiFi Enabled" |
|
||||
checked={wifiEnabled} |
|
||||
onChange={(e) => setWifiEnabled(e.target.checked)} |
|
||||
/> |
|
||||
<input |
|
||||
aria-label="SSID" |
|
||||
value={ssid} |
|
||||
onChange={(e) => setSsid(e.target.value)} |
|
||||
disabled={!wifiEnabled} |
|
||||
/> |
|
||||
<input |
|
||||
aria-label="PSK" |
|
||||
value={psk} |
|
||||
onChange={(e) => setPsk(e.target.value)} |
|
||||
disabled={!wifiEnabled} |
|
||||
/> |
|
||||
<button type="submit" data-testid="submit-button"> |
|
||||
Submit |
|
||||
</button> |
|
||||
</form> |
|
||||
); |
|
||||
}, |
|
||||
}; |
|
||||
}); |
|
||||
|
|
||||
describe("Network component", () => { |
|
||||
const setWorkingConfigMock = vi.fn(); |
|
||||
const mockNetworkConfig = { |
|
||||
wifiEnabled: false, |
|
||||
wifiSsid: "", |
|
||||
wifiPsk: "", |
|
||||
ntpServer: "", |
|
||||
ethEnabled: false, |
|
||||
addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, |
|
||||
ipv4Config: { |
|
||||
ip: 0, |
|
||||
gateway: 0, |
|
||||
subnet: 0, |
|
||||
dns: 0, |
|
||||
}, |
|
||||
enabledProtocols: |
|
||||
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST, |
|
||||
rsyslogServer: "", |
|
||||
}; |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
vi.resetAllMocks(); |
|
||||
|
|
||||
useDevice.mockReturnValue({ |
|
||||
config: { |
|
||||
network: mockNetworkConfig, |
|
||||
}, |
|
||||
setWorkingConfig: setWorkingConfigMock, |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
afterEach(() => { |
|
||||
vi.clearAllMocks(); |
|
||||
}); |
|
||||
|
|
||||
it("should render the Network form", () => { |
|
||||
render(<Network />); |
|
||||
expect(screen.getByTestId("dynamic-form")).toBeInTheDocument(); |
|
||||
}); |
|
||||
|
|
||||
it("should disable SSID and PSK fields when wifi is off", () => { |
|
||||
render(<Network />); |
|
||||
expect(screen.getByLabelText("SSID")).toBeDisabled(); |
|
||||
expect(screen.getByLabelText("PSK")).toBeDisabled(); |
|
||||
}); |
|
||||
|
|
||||
it("should enable SSID and PSK when wifi is toggled on", async () => { |
|
||||
render(<Network />); |
|
||||
const toggle = screen.getByLabelText("WiFi Enabled"); |
|
||||
fireEvent.click(toggle); // turns wifiEnabled = true
|
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(screen.getByLabelText("SSID")).not.toBeDisabled(); |
|
||||
expect(screen.getByLabelText("PSK")).not.toBeDisabled(); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("should call setWorkingConfig with the right structure on submit", async () => { |
|
||||
render(<Network />); |
|
||||
|
|
||||
fireEvent.click(screen.getByTestId("submit-button")); |
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(setWorkingConfigMock).toHaveBeenCalledWith( |
|
||||
expect.objectContaining({ |
|
||||
payloadVariant: { |
|
||||
case: "network", |
|
||||
value: expect.objectContaining({ |
|
||||
wifiEnabled: false, |
|
||||
wifiSsid: "", |
|
||||
wifiPsk: "", |
|
||||
ntpServer: "", |
|
||||
ethEnabled: false, |
|
||||
rsyslogServer: "", |
|
||||
}), |
|
||||
}, |
|
||||
}), |
|
||||
); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it("should submit valid data after enabling wifi and entering SSID and PSK", async () => { |
|
||||
render(<Network />); |
|
||||
fireEvent.click(screen.getByLabelText("WiFi Enabled")); |
|
||||
|
|
||||
fireEvent.change(screen.getByLabelText("SSID"), { |
|
||||
target: { value: "MySSID" }, |
|
||||
}); |
|
||||
|
|
||||
fireEvent.change(screen.getByLabelText("PSK"), { |
|
||||
target: { value: "MySecretPSK" }, |
|
||||
}); |
|
||||
|
|
||||
fireEvent.click(screen.getByTestId("submit-button")); |
|
||||
|
|
||||
await waitFor(() => { |
|
||||
expect(setWorkingConfigMock).toHaveBeenCalledWith( |
|
||||
expect.objectContaining({ |
|
||||
payloadVariant: { |
|
||||
case: "network", |
|
||||
value: expect.objectContaining({ |
|
||||
wifiEnabled: true, |
|
||||
wifiSsid: "MySSID", |
|
||||
wifiPsk: "MySecretPSK", |
|
||||
}), |
|
||||
}, |
|
||||
}), |
|
||||
); |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,142 +1,128 @@ |
|||||
import { describe, expect, it } from "vitest"; |
import { describe, expect, it } from "vitest"; |
||||
import { fireEvent, render, screen } from "@testing-library/react"; |
import { fireEvent, render, screen } from "@testing-library/react"; |
||||
import { Table } from "@components/generic/Table/index.tsx"; |
import { DataRow, Heading, Table } from "@components/generic/Table/index.tsx"; |
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx"; |
import { TimeAgo } from "@components/generic/TimeAgo.tsx"; |
||||
import { Mono } from "@components/generic/Mono.tsx"; |
import { Mono } from "@components/generic/Mono.tsx"; |
||||
// @ts-types="react"
|
// @ts-types="react"
|
||||
|
|
||||
describe("Generic Table", () => { |
describe("Generic Table", () => { |
||||
it("Can render an empty table.", () => { |
it("Can render an empty table.", () => { |
||||
render( |
render(<Table headings={[]} rows={[]} />); |
||||
<Table |
|
||||
headings={[]} |
|
||||
rows={[]} |
|
||||
/>, |
|
||||
); |
|
||||
expect(screen.getByRole("table")).toBeInTheDocument(); |
expect(screen.getByRole("table")).toBeInTheDocument(); |
||||
}); |
}); |
||||
|
|
||||
it("Can render a table with headers and no rows.", async () => { |
it("Can render a table with headers and no rows.", async () => { |
||||
render( |
const headings: Heading[] = [ |
||||
<Table |
{ title: "Short Name", sortable: true }, |
||||
headings={[ |
{ title: "Last Heard", sortable: true }, |
||||
{ title: "", type: "blank", sortable: false }, |
{ title: "Connection", sortable: true }, |
||||
{ title: "Short Name", type: "normal", sortable: true }, |
]; |
||||
{ title: "Long Name", type: "normal", sortable: true }, |
render(<Table headings={headings} rows={[]} />); |
||||
{ title: "Model", type: "normal", sortable: true }, |
|
||||
{ title: "MAC Address", type: "normal", sortable: true }, |
|
||||
{ title: "Last Heard", type: "normal", sortable: true }, |
|
||||
{ title: "SNR", type: "normal", sortable: true }, |
|
||||
{ title: "Encryption", type: "normal", sortable: false }, |
|
||||
{ title: "Connection", type: "normal", sortable: true }, |
|
||||
]} |
|
||||
rows={[]} |
|
||||
/>, |
|
||||
); |
|
||||
await screen.findByRole("table"); |
await screen.findByRole("table"); |
||||
expect(screen.getAllByRole("columnheader")).toHaveLength(9); |
expect(screen.getAllByRole("columnheader")).toHaveLength(3); |
||||
}); |
}); |
||||
|
|
||||
// A simplified version of the rows in pages/Nodes.tsx for testing purposes
|
// Mock data representing devices
|
||||
const mockDevicesWithShortNameAndConnection = [ |
const mockDevices = [ |
||||
{ |
{ |
||||
user: { shortName: "TST1" }, |
id: "TST1", |
||||
|
shortName: "TST1", |
||||
hopsAway: 1, |
hopsAway: 1, |
||||
lastHeard: Date.now() + 1000, |
lastHeard: Date.now() - 3000, |
||||
viaMqtt: false, |
viaMqtt: false, |
||||
}, |
}, |
||||
{ |
{ |
||||
user: { shortName: "TST2" }, |
id: "TST2", |
||||
|
shortName: "TST2", |
||||
hopsAway: 0, |
hopsAway: 0, |
||||
lastHeard: Date.now() + 4000, |
lastHeard: Date.now() - 1000, |
||||
viaMqtt: true, |
viaMqtt: true, |
||||
|
isFavorite: true, // Favorite device
|
||||
}, |
}, |
||||
{ |
{ |
||||
user: { shortName: "TST3" }, |
id: "TST3", |
||||
|
shortName: "TST3", |
||||
hopsAway: 4, |
hopsAway: 4, |
||||
lastHeard: Date.now(), |
lastHeard: Date.now() - 5000, |
||||
viaMqtt: false, |
viaMqtt: false, |
||||
}, |
}, |
||||
{ |
{ |
||||
user: { shortName: "TST4" }, |
id: "TST4", |
||||
|
shortName: "TST4", |
||||
hopsAway: 3, |
hopsAway: 3, |
||||
lastHeard: Date.now() + 2000, |
lastHeard: Date.now() - 2000, |
||||
viaMqtt: true, |
viaMqtt: true, |
||||
}, |
}, |
||||
]; |
]; |
||||
|
|
||||
const mockRows = mockDevicesWithShortNameAndConnection.map((node) => [ |
// Transform mock data into the format expected by the Table component
|
||||
<h1 data-testshortname key={node.user.shortName}>{node.user.shortName}</h1>, |
const mockRows: DataRow[] = mockDevices.map((node) => ({ |
||||
<Mono key="lastHeard" data-testheard> |
id: node.id, |
||||
<TimeAgo timestamp={node.lastHeard * 1000} /> |
isFavorite: node.isFavorite, |
||||
</Mono>, |
cells: [ |
||||
<Mono key="hops" data-testhops> |
{ |
||||
|
content: <b data-testid="short-name">{node.shortName}</b>, |
||||
|
sortValue: node.shortName, |
||||
|
}, |
||||
|
{ |
||||
|
content: ( |
||||
|
<Mono> |
||||
|
<TimeAgo timestamp={node.lastHeard} /> |
||||
|
</Mono> |
||||
|
), |
||||
|
sortValue: node.lastHeard, |
||||
|
}, |
||||
|
{ |
||||
|
content: ( |
||||
|
<Mono> |
||||
{node.lastHeard !== 0 |
{node.lastHeard !== 0 |
||||
? node.viaMqtt === false && node.hopsAway === 0 |
? node.viaMqtt === false && node.hopsAway === 0 |
||||
? "Direct" |
? "Direct" |
||||
: `${node.hopsAway?.toString()} ${ |
: `${node.hopsAway} ${node.hopsAway > 1 ? "hops" : "hop"} away` |
||||
node.hopsAway ?? 0 > 1 ? "hops" : "hop" |
|
||||
} away` |
|
||||
: "-"} |
: "-"} |
||||
{node.viaMqtt === true ? ", via MQTT" : ""} |
{node.viaMqtt ? ", via MQTT" : ""} |
||||
</Mono>, |
</Mono> |
||||
]); |
), |
||||
|
sortValue: node.hopsAway, |
||||
|
}, |
||||
|
], |
||||
|
})); |
||||
|
|
||||
it("Can sort rows appropriately.", async () => { |
const headings: Heading[] = [ |
||||
render( |
{ title: "Short Name", sortable: true }, |
||||
<Table |
{ title: "Last Heard", sortable: true }, |
||||
headings={[ |
{ title: "Connection", sortable: true }, |
||||
{ title: "Short Name", type: "normal", sortable: true }, |
]; |
||||
{ title: "Last Heard", type: "normal", sortable: true }, |
|
||||
{ title: "Connection", type: "normal", sortable: true }, |
it("Can sort rows, keeping favorites at the top", async () => { |
||||
]} |
render(<Table headings={headings} rows={mockRows} />); |
||||
rows={mockRows} |
|
||||
/>, |
|
||||
); |
|
||||
const renderedTable = await screen.findByRole("table"); |
const renderedTable = await screen.findByRole("table"); |
||||
const columnHeaders = screen.getAllByRole("columnheader"); |
const columnHeaders = screen.getAllByRole("columnheader"); |
||||
expect(columnHeaders).toHaveLength(3); |
expect(columnHeaders).toHaveLength(3); |
||||
|
|
||||
// Will be sorted "Last heard" "asc" by default
|
const getRenderedOrder = () => |
||||
expect( |
[...renderedTable.querySelectorAll("[data-testid='short-name']")].map( |
||||
[...renderedTable.querySelectorAll("[data-testshortname]")] |
(el) => el.textContent?.trim(), |
||||
.map((el) => el.textContent) |
); |
||||
.map((v) => v?.trim()) |
|
||||
.join(","), |
|
||||
) |
|
||||
.toMatch("TST2,TST4,TST1,TST3"); |
|
||||
|
|
||||
fireEvent.click(columnHeaders[0]); |
|
||||
|
|
||||
// Re-sort by Short Name asc
|
// Default sort: "Last Heard" desc. TST2 is favorite, so it's first.
|
||||
expect( |
// Then the rest are sorted by lastHeard timestamp (most recent first).
|
||||
[...renderedTable.querySelectorAll("[data-testshortname]")] |
// Order of timestamps: TST2 (latest, but favorite), TST4, TST1, TST3 (oldest).
|
||||
.map((el) => el.textContent) |
expect(getRenderedOrder()).toEqual(["TST2", "TST4", "TST1", "TST3"]); |
||||
.map((v) => v?.trim()) |
|
||||
.join(","), |
|
||||
) |
|
||||
.toMatch("TST1,TST2,TST3,TST4"); |
|
||||
|
|
||||
|
// Click "Short Name" to sort asc
|
||||
fireEvent.click(columnHeaders[0]); |
fireEvent.click(columnHeaders[0]); |
||||
|
// TST2 is favorite, so it's first. Then TST1, TST3, TST4 alphabetically.
|
||||
|
expect(getRenderedOrder()).toEqual(["TST2", "TST1", "TST3", "TST4"]); |
||||
|
|
||||
// Re-sort by Short Name desc
|
// Click "Short Name" again to sort desc
|
||||
expect( |
fireEvent.click(columnHeaders[0]); |
||||
[...renderedTable.querySelectorAll("[data-testshortname]")] |
// TST2 is favorite, so it's first. Then TST4, TST3, TST1 reverse alphabetically.
|
||||
.map((el) => el.textContent) |
expect(getRenderedOrder()).toEqual(["TST2", "TST4", "TST3", "TST1"]); |
||||
.map((v) => v?.trim()) |
|
||||
.join(","), |
|
||||
) |
|
||||
.toMatch("TST4,TST3,TST2,TST1"); |
|
||||
|
|
||||
|
// Click "Connection" to sort by hops asc
|
||||
fireEvent.click(columnHeaders[2]); |
fireEvent.click(columnHeaders[2]); |
||||
|
// TST2 is favorite (and also has 0 hops). Then sorted by hops: TST1 (1), TST4 (3), TST3 (4).
|
||||
// Re-sort by Hops Away
|
expect(getRenderedOrder()).toEqual(["TST2", "TST1", "TST4", "TST3"]); |
||||
expect( |
|
||||
[...renderedTable.querySelectorAll("[data-testshortname]")] |
|
||||
.map((el) => el.textContent) |
|
||||
.map((v) => v?.trim()) |
|
||||
.join(","), |
|
||||
) |
|
||||
.toMatch("TST2,TST1,TST4,TST3"); |
|
||||
}); |
}); |
||||
}); |
}); |
||||
|
|||||
@ -1,68 +0,0 @@ |
|||||
import { act, renderHook } from "@testing-library/react"; |
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"; |
|
||||
import { usePinnedItems } from "./usePinnedItems.ts"; |
|
||||
|
|
||||
const mockSetPinnedItems = vi.fn(); |
|
||||
const mockUseLocalStorage = vi.fn(); |
|
||||
|
|
||||
vi.mock("@core/hooks/useLocalStorage.ts", () => ({ |
|
||||
default: (...args) => mockUseLocalStorage(...args), |
|
||||
})); |
|
||||
|
|
||||
describe("usePinnedItems", () => { |
|
||||
beforeEach(() => { |
|
||||
vi.clearAllMocks(); |
|
||||
}); |
|
||||
|
|
||||
it("returns default pinnedItems and togglePinnedItem", () => { |
|
||||
mockUseLocalStorage.mockReturnValue([[], mockSetPinnedItems]); |
|
||||
|
|
||||
const { result } = renderHook(() => |
|
||||
usePinnedItems({ storageName: "test-storage" }) |
|
||||
); |
|
||||
|
|
||||
expect(result.current.pinnedItems).toEqual([]); |
|
||||
expect(typeof result.current.togglePinnedItem).toBe("function"); |
|
||||
}); |
|
||||
|
|
||||
it("adds an item if it's not already pinned", () => { |
|
||||
mockUseLocalStorage.mockReturnValue([["item1"], mockSetPinnedItems]); |
|
||||
|
|
||||
const { result } = renderHook(() => |
|
||||
usePinnedItems({ storageName: "test-storage" }) |
|
||||
); |
|
||||
|
|
||||
act(() => { |
|
||||
result.current.togglePinnedItem("item2"); |
|
||||
}); |
|
||||
|
|
||||
expect(mockSetPinnedItems).toHaveBeenCalledWith(expect.any(Function)); |
|
||||
|
|
||||
const updater = mockSetPinnedItems.mock.calls[0][0]; |
|
||||
const updated = updater(["item1"]); |
|
||||
|
|
||||
expect(updated).toEqual(["item1", "item2"]); |
|
||||
}); |
|
||||
|
|
||||
it("removes an item if it's already pinned", () => { |
|
||||
mockUseLocalStorage.mockReturnValue([ |
|
||||
["item1", "item2"], |
|
||||
mockSetPinnedItems, |
|
||||
]); |
|
||||
|
|
||||
const { result } = renderHook(() => |
|
||||
usePinnedItems({ storageName: "test-storage" }) |
|
||||
); |
|
||||
|
|
||||
act(() => { |
|
||||
result.current.togglePinnedItem("item1"); |
|
||||
}); |
|
||||
|
|
||||
expect(mockSetPinnedItems).toHaveBeenCalledWith(expect.any(Function)); |
|
||||
|
|
||||
const updater = mockSetPinnedItems.mock.calls[0][0]; |
|
||||
const updated = updater(["item1", "item2"]); |
|
||||
|
|
||||
expect(updated).toEqual(["item2"]); |
|
||||
}); |
|
||||
}); |
|
||||
@ -0,0 +1,82 @@ |
|||||
|
import { vi } from "vitest"; |
||||
|
import { type Device } from "./deviceStore.ts"; |
||||
|
import { Protobuf } from "@meshtastic/core"; |
||||
|
|
||||
|
/** |
||||
|
* You can spread this base mock in your tests and override only the |
||||
|
* properties relevant to a specific test case. |
||||
|
* |
||||
|
* @example |
||||
|
* vi.mocked(useDevice).mockReturnValue({ |
||||
|
* ...mockDeviceStore, |
||||
|
* getNode: (nodeNum) => mockNodes.get(nodeNum), |
||||
|
* }); |
||||
|
*/ |
||||
|
export const mockDeviceStore: Device = { |
||||
|
id: 0, |
||||
|
status: 5 as const, |
||||
|
channels: new Map(), |
||||
|
config: {} as Protobuf.LocalOnly.LocalConfig, |
||||
|
moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig, |
||||
|
workingConfig: [], |
||||
|
workingModuleConfig: [], |
||||
|
hardware: {} as Protobuf.Mesh.MyNodeInfo, |
||||
|
metadata: new Map(), |
||||
|
traceroutes: new Map(), |
||||
|
nodeErrors: new Map(), |
||||
|
connection: undefined, |
||||
|
activeNode: 0, |
||||
|
waypoints: [], |
||||
|
pendingSettingsChanges: false, |
||||
|
messageDraft: "", |
||||
|
unreadCounts: new Map(), |
||||
|
nodesMap: new Map(), |
||||
|
dialog: { |
||||
|
import: false, |
||||
|
QR: false, |
||||
|
shutdown: false, |
||||
|
reboot: false, |
||||
|
rebootOTA: false, |
||||
|
deviceName: false, |
||||
|
nodeRemoval: false, |
||||
|
pkiBackup: false, |
||||
|
nodeDetails: false, |
||||
|
unsafeRoles: false, |
||||
|
refreshKeys: false, |
||||
|
deleteMessages: false, |
||||
|
}, |
||||
|
setStatus: vi.fn(), |
||||
|
setConfig: vi.fn(), |
||||
|
setModuleConfig: vi.fn(), |
||||
|
setWorkingConfig: vi.fn(), |
||||
|
setWorkingModuleConfig: vi.fn(), |
||||
|
setHardware: vi.fn(), |
||||
|
setActiveNode: vi.fn(), |
||||
|
setPendingSettingsChanges: vi.fn(), |
||||
|
addChannel: vi.fn(), |
||||
|
addWaypoint: vi.fn(), |
||||
|
addNodeInfo: vi.fn(), |
||||
|
addUser: vi.fn(), |
||||
|
addPosition: vi.fn(), |
||||
|
addConnection: vi.fn(), |
||||
|
addTraceRoute: vi.fn(), |
||||
|
addMetadata: vi.fn(), |
||||
|
removeNode: vi.fn(), |
||||
|
setDialogOpen: vi.fn(), |
||||
|
getDialogOpen: vi.fn().mockReturnValue(false), |
||||
|
processPacket: vi.fn(), |
||||
|
setMessageDraft: vi.fn(), |
||||
|
setNodeError: vi.fn(), |
||||
|
clearNodeError: vi.fn(), |
||||
|
getNodeError: vi.fn().mockReturnValue(undefined), |
||||
|
hasNodeError: vi.fn().mockReturnValue(false), |
||||
|
incrementUnread: vi.fn(), |
||||
|
resetUnread: vi.fn(), |
||||
|
getNodes: vi.fn().mockReturnValue([]), |
||||
|
getNodesLength: vi.fn().mockReturnValue(0), |
||||
|
getNode: vi.fn().mockReturnValue(undefined), |
||||
|
getMyNode: vi.fn(), |
||||
|
sendAdminMessage: vi.fn(), |
||||
|
updateFavorite: vi.fn(), |
||||
|
updateIgnored: vi.fn(), |
||||
|
}; |
||||
@ -0,0 +1,18 @@ |
|||||
|
export function intlSort<T extends PropertyKey>( |
||||
|
arr: T[], |
||||
|
order: "asc" | "desc" = "asc", |
||||
|
locale: Intl.Locale, |
||||
|
): T[] { |
||||
|
const collator = new Intl.Collator(locale, { sensitivity: "base" }); |
||||
|
|
||||
|
return arr.sort((a, b) => { |
||||
|
const stringA = String(a); |
||||
|
const stringB = String(b); |
||||
|
|
||||
|
if (order === "asc") { |
||||
|
return collator.compare(stringA, stringB); |
||||
|
} else { |
||||
|
return collator.compare(stringB, stringA); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
@ -1,80 +0,0 @@ |
|||||
import { |
|
||||
createMemoryHistory, |
|
||||
createRouter, |
|
||||
Outlet, |
|
||||
RootRoute, |
|
||||
Route, |
|
||||
RouterProvider, |
|
||||
} from "@tanstack/react-router"; |
|
||||
import { render as rtlRender, RenderOptions } from "@testing-library/react"; |
|
||||
import type { FunctionComponent, ReactElement, ReactNode } from "react"; |
|
||||
|
|
||||
// a root route for the test router.
|
|
||||
const rootRoute = new RootRoute({ |
|
||||
component: () => ( |
|
||||
<> |
|
||||
<Outlet /> |
|
||||
</> |
|
||||
), |
|
||||
}); |
|
||||
|
|
||||
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> { |
|
||||
initialEntries?: string[]; |
|
||||
ui?: ReactElement; |
|
||||
} |
|
||||
|
|
||||
let currentRouter: ReturnType<typeof createRouter> | null = null; |
|
||||
|
|
||||
/** |
|
||||
* Custom render function for testing components that need TanStack Router context. |
|
||||
* @param ui The main ReactElement to render (your component under test). |
|
||||
* @param options Custom render options including initialEntries for the router. |
|
||||
* @returns An object containing the testing-library render result and the router instance. |
|
||||
*/ |
|
||||
const customRender = ( |
|
||||
ui: ReactElement, |
|
||||
options: CustomRenderOptions = {}, |
|
||||
) => { |
|
||||
const { initialEntries = ["/"], ...renderOptions } = options; |
|
||||
|
|
||||
// A specific route that renders the component under test (ui).
|
|
||||
// It defaults to the first path in initialEntries or '/'.
|
|
||||
const testComponentRoute = new Route({ |
|
||||
getParentRoute: () => rootRoute, |
|
||||
path: initialEntries[0] || "/", |
|
||||
component: () => ui, // The component passed to render will be the element for this route
|
|
||||
}); |
|
||||
|
|
||||
const routeTree = rootRoute.addChildren([testComponentRoute]); |
|
||||
|
|
||||
const router = createRouter({ |
|
||||
history: createMemoryHistory({ initialEntries }), |
|
||||
routeTree, |
|
||||
// You can add default error components or other router options if needed for tests.
|
|
||||
// defaultErrorComponent: ({ error }) => <div>Test Error: {error.message}</div>,
|
|
||||
}); |
|
||||
|
|
||||
currentRouter = router; // Store the router instance for access in tests
|
|
||||
|
|
||||
const Wrapper: FunctionComponent<{ children?: ReactNode }> = ( |
|
||||
{ children }, |
|
||||
) => { |
|
||||
return ( |
|
||||
<> |
|
||||
<RouterProvider router={router} /> |
|
||||
{children} |
|
||||
</> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
const renderResult = rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); |
|
||||
|
|
||||
return { |
|
||||
...renderResult, |
|
||||
router, |
|
||||
}; |
|
||||
}; |
|
||||
|
|
||||
export * from "@testing-library/react"; |
|
||||
export { customRender as render }; |
|
||||
export const getTestRouter = () => currentRouter; |
|
||||
@ -1,81 +0,0 @@ |
|||||
import { beforeEach, describe, expect, it, vi } from "vitest"; |
|
||||
import { fireEvent, render, screen } from "@testing-library/react"; |
|
||||
import { MessagesPage } from "./Messages.tsx"; |
|
||||
import { useDevice } from "../core/stores/deviceStore.ts"; |
|
||||
import { Protobuf } from "@meshtastic/core"; |
|
||||
|
|
||||
vi.mock("../core/stores/deviceStore", () => ({ |
|
||||
useDevice: vi.fn(), |
|
||||
})); |
|
||||
|
|
||||
const mockUseDevice = { |
|
||||
channels: new Map([ |
|
||||
[0, { |
|
||||
index: 0, |
|
||||
settings: { name: "Primary" }, |
|
||||
role: Protobuf.Channel.Channel_Role.PRIMARY, |
|
||||
}], |
|
||||
]), |
|
||||
nodes: new Map([ |
|
||||
[0, { |
|
||||
num: 0, |
|
||||
user: { longName: "Test Node 0", shortName: "TN0", publicKey: "0000" }, |
|
||||
}], |
|
||||
[1111, { |
|
||||
num: 1111, |
|
||||
user: { longName: "Test Node 1", shortName: "TN1", publicKey: "12345" }, |
|
||||
}], |
|
||||
[2222, { |
|
||||
num: 2222, |
|
||||
user: { longName: "Test Node 2", shortName: "TN2", publicKey: "67890" }, |
|
||||
}], |
|
||||
[3333, { |
|
||||
num: 3333, |
|
||||
user: { longName: "Test Node 3", shortName: "TN3", publicKey: "11111" }, |
|
||||
}], |
|
||||
]), |
|
||||
hardware: { myNodeNum: 1 }, |
|
||||
messages: { broadcast: new Map(), direct: new Map() }, |
|
||||
metadata: new Map(), |
|
||||
unreadCounts: new Map([[1111, 3], [2222, 10]]), |
|
||||
resetUnread: vi.fn(), |
|
||||
hasNodeError: vi.fn(), |
|
||||
}; |
|
||||
|
|
||||
describe.skip("Messages Page", () => { |
|
||||
beforeEach(() => { |
|
||||
vi.mocked(useDevice).mockReturnValue(mockUseDevice); |
|
||||
}); |
|
||||
|
|
||||
it("sorts unreads to the top", () => { |
|
||||
render(<MessagesPage />); |
|
||||
const buttonOrder = screen.getAllByRole("button").filter((b) => |
|
||||
b.textContent.includes("Test Node") |
|
||||
); |
|
||||
expect(buttonOrder[0].textContent).toContain("TN2Test Node 210"); |
|
||||
expect(buttonOrder[1].textContent).toContain("TN1Test Node 13"); |
|
||||
expect(buttonOrder[2].textContent).toContain("TN0Test Node 0"); |
|
||||
expect(buttonOrder[3].textContent).toContain("TN3Test Node 3"); |
|
||||
}); |
|
||||
|
|
||||
it("updates unread when active chat changes", () => { |
|
||||
render(<MessagesPage />); |
|
||||
const nodeButton = |
|
||||
screen.getAllByRole("button").filter((b) => |
|
||||
b.textContent.includes("TN1Test Node 13") |
|
||||
)[0]; |
|
||||
fireEvent.click(nodeButton); |
|
||||
expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0); |
|
||||
}); |
|
||||
|
|
||||
it("does not update the incorrect node", () => { |
|
||||
render(<MessagesPage />); |
|
||||
const nodeButton = |
|
||||
screen.getAllByRole("button").filter((b) => |
|
||||
b.textContent.includes("TN1Test Node 1") |
|
||||
)[0]; |
|
||||
fireEvent.click(nodeButton); |
|
||||
expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0); |
|
||||
expect(mockUseDevice.unreadCounts.get(2222)).toBe(10); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,59 +0,0 @@ |
|||||
/* eslint-disable */ |
|
||||
|
|
||||
// @ts-nocheck
|
|
||||
|
|
||||
// noinspection JSUnusedGlobalSymbols
|
|
||||
|
|
||||
// This file was automatically generated by TanStack Router.
|
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
||||
|
|
||||
// Import Routes
|
|
||||
|
|
||||
import { Route as rootRoute } from './routes/__root' |
|
||||
|
|
||||
// Create/Update Routes
|
|
||||
|
|
||||
// Populate the FileRoutesByPath interface
|
|
||||
|
|
||||
declare module '@tanstack/react-router' { |
|
||||
interface FileRoutesByPath {} |
|
||||
} |
|
||||
|
|
||||
// Create and export the route tree
|
|
||||
|
|
||||
export interface FileRoutesByFullPath {} |
|
||||
|
|
||||
export interface FileRoutesByTo {} |
|
||||
|
|
||||
export interface FileRoutesById { |
|
||||
__root__: typeof rootRoute |
|
||||
} |
|
||||
|
|
||||
export interface FileRouteTypes { |
|
||||
fileRoutesByFullPath: FileRoutesByFullPath |
|
||||
fullPaths: never |
|
||||
fileRoutesByTo: FileRoutesByTo |
|
||||
to: never |
|
||||
id: '__root__' |
|
||||
fileRoutesById: FileRoutesById |
|
||||
} |
|
||||
|
|
||||
export interface RootRouteChildren {} |
|
||||
|
|
||||
const rootRouteChildren: RootRouteChildren = {} |
|
||||
|
|
||||
export const routeTree = rootRoute |
|
||||
._addFileChildren(rootRouteChildren) |
|
||||
._addFileTypes<FileRouteTypes>() |
|
||||
|
|
||||
/* ROUTE_MANIFEST_START |
|
||||
{ |
|
||||
"routes": { |
|
||||
"__root__": { |
|
||||
"filePath": "__root.tsx", |
|
||||
"children": [] |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
ROUTE_MANIFEST_END */ |
|
||||
@ -0,0 +1,37 @@ |
|||||
|
import { ReactElement } from "react"; |
||||
|
import { render, RenderOptions } from "@testing-library/react"; |
||||
|
import { |
||||
|
createMemoryHistory, |
||||
|
createRouter, |
||||
|
RouterProvider, |
||||
|
} from "@tanstack/react-router"; |
||||
|
import "../i18n/config.ts"; |
||||
|
import { routeTree } from "../routeTree.gen.ts"; |
||||
|
|
||||
|
import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; |
||||
|
|
||||
|
const Providers = () => { |
||||
|
const memoryHistory = createMemoryHistory({ |
||||
|
initialEntries: ["/"], |
||||
|
}); |
||||
|
|
||||
|
const router = createRouter({ |
||||
|
routeTree, |
||||
|
history: memoryHistory, |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<DeviceWrapper> |
||||
|
<RouterProvider router={router} /> |
||||
|
</DeviceWrapper> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const renderWithProviders = ( |
||||
|
ui: ReactElement, |
||||
|
options?: Omit<RenderOptions, "wrapper">, |
||||
|
) => render(ui, { wrapper: Providers, ...options }); |
||||
|
|
||||
|
export * from "@testing-library/react"; |
||||
|
|
||||
|
export { renderWithProviders as render }; |
||||
@ -0,0 +1,11 @@ |
|||||
|
/// <reference types="vite/client" />
|
||||
|
|
||||
|
interface ImportMetaEnv { |
||||
|
readonly env: { |
||||
|
readonly VITE_COMMIT_HASH: string; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
interface ImportMeta { |
||||
|
readonly env: ImportMetaEnv; |
||||
|
} |
||||
Loading…
Reference in new issue