From c55fdbd98241bec6cc831e6e1fcdeb36930b2cb7 Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Fri, 28 Mar 2025 11:45:48 -0400 Subject: [PATCH 1/3] wip --- deno.json | 5 + deno.lock | 115 +++++++++--------- package.json | 5 +- src/components/Form/FormSelect.tsx | 9 +- .../PageComponents/Config/Network.tsx | 23 +++- src/core/utils/ip.ts | 10 +- src/validation/config/network.ts | 5 +- 7 files changed, 103 insertions(+), 69 deletions(-) diff --git a/deno.json b/deno.json index 2ab367be..b5ed748e 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,10 @@ { "imports": { + "@meshtastic/core": "jsr:@meshtastic/core@^2.6.2", + "@meshtastic/js": "jsr:@meshtastic/js@^2.3.4", + "@meshtastic/transport-http": "jsr:@meshtastic/transport-http@^0.2.1", + "@meshtastic/transport-web-bluetooth": "jsr:@meshtastic/transport-web-bluetooth@^0.1.1", + "@meshtastic/transport-web-serial": "jsr:@meshtastic/transport-web-serial@^0.2.1", "@app/": "./src/", "@pages/": "./src/pages/", "@components/": "./src/components/", diff --git a/deno.lock b/deno.lock index 46190061..7d73615b 100644 --- a/deno.lock +++ b/deno.lock @@ -1,11 +1,15 @@ { "version": "4", "specifiers": { + "jsr:@meshtastic/core@^2.6.0": "2.6.2", + "jsr:@meshtastic/core@^2.6.2": "2.6.2", + "jsr:@meshtastic/js@^2.3.4": "2.3.4", + "jsr:@meshtastic/protobufs@^2.3.12": "2.6.2", + "jsr:@meshtastic/protobufs@^2.6.2": "2.6.2", + "jsr:@meshtastic/transport-http@~0.2.1": "0.2.1", + "jsr:@meshtastic/transport-web-bluetooth@~0.1.1": "0.1.1", + "jsr:@meshtastic/transport-web-serial@~0.2.1": "0.2.1", "npm:@bufbuild/protobuf@^2.2.3": "2.2.3", - "npm:@jsr/meshtastic__core@2.6.0-0": "2.6.0-0", - "npm:@jsr/meshtastic__js@2.6.0-0": "2.6.0-0", - "npm:@jsr/meshtastic__transport-http@*": "0.2.1", - "npm:@jsr/meshtastic__transport-web-serial@*": "0.2.1", "npm:@noble/curves@^1.8.1": "1.8.1", "npm:@radix-ui/react-accordion@^1.2.3": "1.2.3_@types+react@19.0.10_@types+react-dom@19.0.4__@types+react@19.0.10_react@19.0.0_react-dom@19.0.0__react@19.0.0", "npm:@radix-ui/react-checkbox@^1.1.4": "1.1.4_@types+react@19.0.10_@types+react-dom@19.0.4__@types+react@19.0.10_react@19.0.0_react-dom@19.0.0__react@19.0.0", @@ -73,8 +77,48 @@ "npm:vite@*": "6.2.0_@types+node@22.13.8", "npm:vite@^6.2.0": "6.2.0_@types+node@22.13.8", "npm:vitest@^3.0.7": "3.0.8_@types+node@22.13.8_happy-dom@17.2.2_vite@6.2.0__@types+node@22.13.8_@vitest+browser@3.0.8__playwright@1.50.1__vitest@3.0.8___@types+node@22.13.8___happy-dom@17.2.2___@vitest+browser@3.0.8____playwright@1.50.1____vitest@3.0.8____msw@2.7.3_____typescript@5.8.2_____@types+node@22.13.8____vite@6.2.0_____@types+node@22.13.8____typescript@5.8.2____@types+node@22.13.8____happy-dom@17.2.2___playwright@1.50.1___vite@6.2.0____@types+node@22.13.8___typescript@5.8.2__vitest@3.0.8__typescript@5.8.2__msw@2.7.3___typescript@5.8.2___@types+node@22.13.8__vite@6.2.0___@types+node@22.13.8__@types+node@22.13.8_playwright@1.50.1_typescript@5.8.2", + "npm:zod@^3.24.2": "3.24.2", "npm:zustand@5.0.3": "5.0.3_@types+react@19.0.10_immer@10.1.1_react@19.0.0" }, + "jsr": { + "@meshtastic/core@2.6.2": { + "integrity": "5c948bbbfad280c5eb093c62edc84773f76509487b333066ec4a349f40dcacf2", + "dependencies": [ + "jsr:@meshtastic/protobufs@^2.6.2", + "npm:@bufbuild/protobuf", + "npm:crc", + "npm:ste-simple-events", + "npm:tslog@^4.9.3" + ] + }, + "@meshtastic/js@2.3.4": { + "integrity": "7a81a36fb7ef1b7b68a3989c02d50f687114ac56bcd7f0452a31ef560ac99719", + "dependencies": [ + "jsr:@meshtastic/protobufs@^2.3.12", + "npm:crc", + "npm:ste-simple-events", + "npm:tslog@^4.9.2" + ] + }, + "@meshtastic/protobufs@2.6.2": { + "integrity": "55e9b98fc22ea0d28e6a7979e4ff0a5f2c94513c1bc93e67522636a89925ad69", + "dependencies": [ + "npm:@bufbuild/protobuf" + ] + }, + "@meshtastic/transport-http@0.2.1": { + "integrity": "4d086ee6d5665c3490736737c4354eb3049edf792b1d195b30a3254cb535a7d6" + }, + "@meshtastic/transport-web-bluetooth@0.1.1": { + "integrity": "f7676b98e2049ad0bca508e34054730b22cf2648019921989f11297441fe958d" + }, + "@meshtastic/transport-web-serial@0.2.1": { + "integrity": "d09fa8ac278b105c8f2b3a72af9cf8a5676baac6f4e9111c6773ff6217e2d5be", + "dependencies": [ + "jsr:@meshtastic/core@^2.6.0" + ] + } + }, "npm": { "@adobe/css-tools@4.4.2": { "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==" @@ -1082,54 +1126,6 @@ "@jridgewell/sourcemap-codec" ] }, - "@jsr/meshtastic__core@2.6.0": { - "integrity": "sha512-+Ik6gzZnfi5sW+WC06bRayA6KGF2NI+zi3bqKbvA8mGDNSOPgsFhA4VZ79DKY4bSflTW170MRIUeyYo0IWQQuw==", - "dependencies": [ - "@bufbuild/protobuf", - "@jsr/meshtastic__protobufs", - "crc", - "ste-simple-events", - "tslog" - ] - }, - "@jsr/meshtastic__core@2.6.0-0": { - "integrity": "sha512-Ks71sRagbBipotznULpsJZ1EMcQIqCEJQx6mf628dmCNVf2YECi2zi/i/5zErp1hGPgfbDvCz9oPogvsd/7fMA==", - "dependencies": [ - "@bufbuild/protobuf", - "@jsr/meshtastic__protobufs", - "crc", - "ste-simple-events", - "tslog" - ] - }, - "@jsr/meshtastic__js@2.6.0-0": { - "integrity": "sha512-+xpZpxK6oUIVOuEs7C+LyxRr2druvc7UNNNTK9Rl8ioXj63Jz1uQXlYe2Gj0xjnRAiSQLR7QVaPef21BR/YTxA==", - "dependencies": [ - "@bufbuild/protobuf", - "@jsr/meshtastic__protobufs", - "crc", - "ste-simple-events", - "tslog" - ] - }, - "@jsr/meshtastic__protobufs@2.6.0": { - "integrity": "sha512-CGlgBdzAuQCZuGPrnzP8zU+EcLlmyYeeMbqFHuJ834cYfArWXDjDh1UYaPo2rI03LTjqa3MeWpfqDlzBR8kIMg==", - "dependencies": [ - "@bufbuild/protobuf" - ] - }, - "@jsr/meshtastic__transport-http@0.2.1": { - "integrity": "sha512-lmQKr3aIINKvtGROU4HchmSVqbZSbkIHqajowRRC8IAjsnR0zNTyxz210QyY4pFUF9hpcW3GRjwq5h/VO2JuGg==", - "dependencies": [ - "@jsr/meshtastic__core@2.6.0" - ] - }, - "@jsr/meshtastic__transport-web-serial@0.2.1": { - "integrity": "sha512-yumjEGLkAuJYOC3aWKvZzbQqi/LnqaKfNpVCY7Ki7oLtAshNiZrBLiwiFhN7+ZR9FfMdJThyBMqREBDRRWTO1Q==", - "dependencies": [ - "@jsr/meshtastic__core@2.6.0" - ] - }, "@mapbox/geojson-rewind@0.5.2": { "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", "dependencies": [ @@ -6869,6 +6865,9 @@ "yoctocolors-cjs@2.1.2": { "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==" }, + "zod@3.24.2": { + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==" + }, "zone.js@0.8.29": { "integrity": "sha512-mla2acNCMkWXBD+c+yeUrBUrzOxYMNFdQ6FGfigGGtEVBPJx07BQeJekjt9DmH1FtZek4E9rE1eRR9qQpxACOQ==" }, @@ -6882,13 +6881,16 @@ } }, "workspace": { + "dependencies": [ + "jsr:@meshtastic/core@^2.6.2", + "jsr:@meshtastic/js@^2.3.4", + "jsr:@meshtastic/transport-http@~0.2.1", + "jsr:@meshtastic/transport-web-bluetooth@~0.1.1", + "jsr:@meshtastic/transport-web-serial@~0.2.1" + ], "packageJson": { "dependencies": [ "npm:@bufbuild/protobuf@^2.2.3", - "npm:@jsr/meshtastic__core@2.6.0-0", - "npm:@jsr/meshtastic__js@2.6.0-0", - "npm:@jsr/meshtastic__transport-http@*", - "npm:@jsr/meshtastic__transport-web-serial@*", "npm:@noble/curves@^1.8.1", "npm:@radix-ui/react-accordion@^1.2.3", "npm:@radix-ui/react-checkbox@^1.1.4", @@ -6950,6 +6952,7 @@ "npm:vite-plugin-pwa@~0.21.1", "npm:vite@^6.2.0", "npm:vitest@^3.0.7", + "npm:zod@^3.24.2", "npm:zustand@5.0.3" ] } diff --git a/package.json b/package.json index 51b99b39..f161c710 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,6 @@ "homepage": "https://meshtastic.org", "dependencies": { "@bufbuild/protobuf": "^2.2.3", - "@meshtastic/core": "npm:@jsr/meshtastic__core@2.6.0-0", - "@meshtastic/js": "npm:@jsr/meshtastic__js@2.6.0-0", - "@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http", - "@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial", "@noble/curves": "^1.8.1", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-checkbox": "^1.1.4", @@ -73,6 +69,7 @@ "react-qrcode-logo": "^3.0.0", "rfc4648": "^1.5.4", "vite-plugin-node-polyfills": "^0.23.0", + "zod": "^3.24.2", "zustand": "5.0.3" }, "devDependencies": { diff --git a/src/components/Form/FormSelect.tsx b/src/components/Form/FormSelect.tsx index 037b651c..414ce12c 100644 --- a/src/components/Form/FormSelect.tsx +++ b/src/components/Form/FormSelect.tsx @@ -10,13 +10,14 @@ import { SelectValue, } from "@components/UI/Select.tsx"; import { useController, type FieldValues } from "react-hook-form"; -import { computeHeadingLevel } from "@core/utils/test.tsx"; export interface SelectFieldProps extends BaseFormBuilderProps { type: "select"; selectChange?: (e: string, name: string) => void; validate?: (newValue: string) => Promise; + defaultValue?: string; properties: BaseFormBuilderProps["properties"] & { + defaultValue?: T; enumValue: { [s: string]: string | number; }; @@ -38,11 +39,15 @@ export function SelectInput({ disabled, field, }: GenericFormElementProps>) { + // Get default value and set it + const defaultValue = field.properties.defaultValue ?? field.defaultValue; + const { field: { value, onChange, ...rest }, } = useController({ name: field.name, control, + defaultValue: defaultValue ? defaultValue.toString() : undefined, }); const { enumValue, formatEnumName, ...remainingProperties } = field.properties; @@ -70,12 +75,12 @@ export function SelectInput({ onChange(Number.parseInt(newValue)); }; - return ( { +// const mockData = { role: e.target.value }; +// onSubmit(mockData); +// }} +// > +// {Object.entries(Protobuf.Config.Config_DeviceConfig_Role).map(([key, value]) => ( +// +// ))} +// +// +// +// ); +// }) +// })); + +// describe('Network component', () => { +// const setWorkingConfigMock = vi.fn(); +// const mockDeviceConfig = { +// role: "CLIENT", +// buttonGpio: 0, +// buzzerGpio: 0, +// rebroadcastMode: "ALL", +// nodeInfoBroadcastSecs: 300, +// doubleTapAsButtonPress: false, +// disableTripleClick: false, +// ledHeartbeatDisabled: false, +// }; + +// beforeEach(() => { +// vi.resetAllMocks(); + +// (useDevice as any).mockReturnValue({ +// config: { +// device: mockDeviceConfig +// }, +// setWorkingConfig: setWorkingConfigMock +// }); + +// }); + +// afterEach(() => { +// vi.clearAllMocks(); +// }); + +// it('should render the Network form', () => { +// render(); +// expect(screen.getByTestId('dynamic-form')).toBeInTheDocument(); +// }); + +// it('should call setWorkingConfig when form is submitted', async () => { +// render(); + +// 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(); + +// // Simulate form submission +// fireEvent.click(screen.getByTestId('submit-button')); + +// await waitFor(() => { +// expect(setWorkingConfigMock).toHaveBeenCalledWith( +// expect.objectContaining({ +// payloadVariant: { +// case: "network", +// value: expect.any(Object) +// } +// }) +// ); +// }); +// }); +// }); + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, 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 }: any) => { + const [wifiEnabled, setWifiEnabled] = useState(defaultValues.wifiEnabled ?? false); + const [ssid, setSsid] = useState(defaultValues.wifiSsid ?? ''); + const [psk, setPsk] = useState(defaultValues.wifiPsk ?? ''); + + return ( +
{ + e.preventDefault(); + onSubmit({ + ...defaultValues, + wifiEnabled, + wifiSsid: ssid, + wifiPsk: psk, + }); + }} + data-testid="dynamic-form" + > + setWifiEnabled(e.target.checked)} + /> + setSsid(e.target.value)} + disabled={!wifiEnabled} + /> + setPsk(e.target.value)} + disabled={!wifiEnabled} + /> + +
+ ); + }, + }; +}); +; + +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 as any).mockReturnValue({ + config: { + network: mockNetworkConfig + }, + setWorkingConfig: setWorkingConfigMock + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render the Network form', () => { + render(); + expect(screen.getByTestId('dynamic-form')).toBeInTheDocument(); + }); + + it('should disable SSID and PSK fields when wifi is off', () => { + render(); + expect(screen.getByLabelText("SSID")).toBeDisabled(); + expect(screen.getByLabelText("PSK")).toBeDisabled(); + }); + + it('should enable SSID and PSK when wifi is toggled on', async () => { + render(); + const toggle = screen.getByLabelText("WiFi Enabled"); + screen.debug() + + 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(); + + 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(); + 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" + }) + } + }) + ); + }); + }); +}); diff --git a/src/components/PageComponents/Config/Network.tsx b/src/components/PageComponents/Config/Network/index.tsx similarity index 99% rename from src/components/PageComponents/Config/Network.tsx rename to src/components/PageComponents/Config/Network/index.tsx index a52e74bd..cae121d6 100644 --- a/src/components/PageComponents/Config/Network.tsx +++ b/src/components/PageComponents/Config/Network/index.tsx @@ -12,8 +12,6 @@ import { validateSchema } from "@app/validation/validate.ts"; export const Network = () => { const { config, setWorkingConfig } = useDevice(); - console.log(config.network); - const onSubmit = (data: NetworkValidation) => { const result = validateSchema(NetworkValidationSchema, data); diff --git a/src/pages/Config/DeviceConfig.tsx b/src/pages/Config/DeviceConfig.tsx index 697b80b9..b97e8640 100644 --- a/src/pages/Config/DeviceConfig.tsx +++ b/src/pages/Config/DeviceConfig.tsx @@ -2,7 +2,7 @@ import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx"; import { Device } from "../../components/PageComponents/Config/Device/index.tsx"; import { Display } from "@components/PageComponents/Config/Display.tsx"; import { LoRa } from "@components/PageComponents/Config/LoRa.tsx"; -import { Network } from "@components/PageComponents/Config/Network.tsx"; +import { Network } from "../../components/PageComponents/Config/Network/index.tsx"; import { Position } from "@components/PageComponents/Config/Position.tsx"; import { Power } from "@components/PageComponents/Config/Power.tsx"; import { Security } from "../../components/PageComponents/Config/Security/Security.tsx";