Merge pull request #547 from danditomaso/issue-523-add-udp-toggle
feat: Add udp over mesh toggle
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"imports": {
|
"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/",
|
"@app/": "./src/",
|
||||||
"@pages/": "./src/pages/",
|
"@pages/": "./src/pages/",
|
||||||
"@components/": "./src/components/",
|
"@components/": "./src/components/",
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"version": "4",
|
"version": "4",
|
||||||
"specifiers": {
|
"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:@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:@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-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",
|
"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_@types+node@22.13.8",
|
||||||
"npm:vite@^6.2.0": "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: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"
|
"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": {
|
"npm": {
|
||||||
"@adobe/css-tools@4.4.2": {
|
"@adobe/css-tools@4.4.2": {
|
||||||
"integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A=="
|
"integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A=="
|
||||||
@@ -1082,54 +1126,6 @@
|
|||||||
"@jridgewell/sourcemap-codec"
|
"@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": {
|
"@mapbox/geojson-rewind@0.5.2": {
|
||||||
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
|
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -6869,6 +6865,9 @@
|
|||||||
"yoctocolors-cjs@2.1.2": {
|
"yoctocolors-cjs@2.1.2": {
|
||||||
"integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="
|
"integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="
|
||||||
},
|
},
|
||||||
|
"zod@3.24.2": {
|
||||||
|
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
|
||||||
|
},
|
||||||
"zone.js@0.8.29": {
|
"zone.js@0.8.29": {
|
||||||
"integrity": "sha512-mla2acNCMkWXBD+c+yeUrBUrzOxYMNFdQ6FGfigGGtEVBPJx07BQeJekjt9DmH1FtZek4E9rE1eRR9qQpxACOQ=="
|
"integrity": "sha512-mla2acNCMkWXBD+c+yeUrBUrzOxYMNFdQ6FGfigGGtEVBPJx07BQeJekjt9DmH1FtZek4E9rE1eRR9qQpxACOQ=="
|
||||||
},
|
},
|
||||||
@@ -6882,13 +6881,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspace": {
|
"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": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@bufbuild/protobuf@^2.2.3",
|
"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:@noble/curves@^1.8.1",
|
||||||
"npm:@radix-ui/react-accordion@^1.2.3",
|
"npm:@radix-ui/react-accordion@^1.2.3",
|
||||||
"npm:@radix-ui/react-checkbox@^1.1.4",
|
"npm:@radix-ui/react-checkbox@^1.1.4",
|
||||||
@@ -6950,6 +6952,7 @@
|
|||||||
"npm:vite-plugin-pwa@~0.21.1",
|
"npm:vite-plugin-pwa@~0.21.1",
|
||||||
"npm:vite@^6.2.0",
|
"npm:vite@^6.2.0",
|
||||||
"npm:vitest@^3.0.7",
|
"npm:vitest@^3.0.7",
|
||||||
|
"npm:zod@^3.24.2",
|
||||||
"npm:zustand@5.0.3"
|
"npm:zustand@5.0.3"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -35,10 +35,6 @@
|
|||||||
"homepage": "https://meshtastic.org",
|
"homepage": "https://meshtastic.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^2.2.3",
|
"@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",
|
"@noble/curves": "^1.8.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.3",
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
@@ -73,6 +69,7 @@
|
|||||||
"react-qrcode-logo": "^3.0.0",
|
"react-qrcode-logo": "^3.0.0",
|
||||||
"rfc4648": "^1.5.4",
|
"rfc4648": "^1.5.4",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
|
"zod": "^3.24.2",
|
||||||
"zustand": "5.0.3"
|
"zustand": "5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@components/UI/Select.tsx";
|
} from "@components/UI/Select.tsx";
|
||||||
import { useController, type FieldValues } from "react-hook-form";
|
import { useController, type FieldValues } from "react-hook-form";
|
||||||
import { computeHeadingLevel } from "@core/utils/test.tsx";
|
|
||||||
|
|
||||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "select";
|
type: "select";
|
||||||
selectChange?: (e: string, name: string) => void;
|
selectChange?: (e: string, name: string) => void;
|
||||||
validate?: (newValue: string) => Promise<boolean>;
|
validate?: (newValue: string) => Promise<boolean>;
|
||||||
|
defaultValue?: string;
|
||||||
properties: BaseFormBuilderProps<T>["properties"] & {
|
properties: BaseFormBuilderProps<T>["properties"] & {
|
||||||
|
defaultValue?: T;
|
||||||
enumValue: {
|
enumValue: {
|
||||||
[s: string]: string | number;
|
[s: string]: string | number;
|
||||||
};
|
};
|
||||||
@@ -70,7 +71,6 @@ export function SelectInput<T extends FieldValues>({
|
|||||||
onChange(Number.parseInt(newValue));
|
onChange(Number.parseInt(newValue));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// 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', () => ({
|
||||||
|
// DynamicForm: vi.fn(({ onSubmit }) => {
|
||||||
|
// return (
|
||||||
|
// <div data-testid="dynamic-form">
|
||||||
|
// <select
|
||||||
|
// data-testid="role-select"
|
||||||
|
// onChange={(e) => {
|
||||||
|
// 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('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(<Network />);
|
||||||
|
// expect(screen.getByTestId('dynamic-form')).toBeInTheDocument();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should call setWorkingConfig when form is submitted', async () => {
|
||||||
|
// render(<Network />);
|
||||||
|
|
||||||
|
// 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(<Network />);
|
||||||
|
|
||||||
|
// // 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 (
|
||||||
|
<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 as any).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");
|
||||||
|
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(<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"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+30
-5
@@ -1,4 +1,4 @@
|
|||||||
import type { NetworkValidation } from "@app/validation/config/network.tsx";
|
import { NetworkValidationSchema, type NetworkValidation } from "@app/validation/config/network.ts";
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
@@ -7,11 +7,18 @@ import {
|
|||||||
convertIpAddressToInt,
|
convertIpAddressToInt,
|
||||||
} from "@core/utils/ip.ts";
|
} from "@core/utils/ip.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
import { validateSchema } from "@app/validation/validate.ts";
|
||||||
|
|
||||||
export const Network = () => {
|
export const Network = () => {
|
||||||
const { config, setWorkingConfig } = useDevice();
|
const { config, setWorkingConfig } = useDevice();
|
||||||
|
|
||||||
const onSubmit = (data: NetworkValidation) => {
|
const onSubmit = (data: NetworkValidation) => {
|
||||||
|
const result = validateSchema(NetworkValidationSchema, data);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("Validation errors:", result.errors);
|
||||||
|
}
|
||||||
|
|
||||||
setWorkingConfig(
|
setWorkingConfig(
|
||||||
create(Protobuf.Config.ConfigSchema, {
|
create(Protobuf.Config.ConfigSchema, {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
@@ -21,10 +28,10 @@ export const Network = () => {
|
|||||||
ipv4Config: create(
|
ipv4Config: create(
|
||||||
Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema,
|
Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema,
|
||||||
{
|
{
|
||||||
ip: convertIpAddressToInt(data.ipv4Config.ip) ?? 0,
|
ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
|
||||||
gateway: convertIpAddressToInt(data.ipv4Config.gateway) ?? 0,
|
gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
|
||||||
subnet: convertIpAddressToInt(data.ipv4Config.subnet) ?? 0,
|
subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
|
||||||
dns: convertIpAddressToInt(data.ipv4Config.dns) ?? 0,
|
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -48,6 +55,8 @@ export const Network = () => {
|
|||||||
),
|
),
|
||||||
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
|
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
|
||||||
},
|
},
|
||||||
|
enabledProtocols: config.network?.enabledProtocols ?? Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST
|
||||||
|
|
||||||
}}
|
}}
|
||||||
fieldGroups={[
|
fieldGroups={[
|
||||||
{
|
{
|
||||||
@@ -165,6 +174,22 @@ export const Network = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "UDP Config",
|
||||||
|
description: "UDP over Mesh configuration",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "select",
|
||||||
|
name: "enabledProtocols",
|
||||||
|
label: "Mesh via UDP",
|
||||||
|
properties: {
|
||||||
|
enumValue:
|
||||||
|
Protobuf.Config.Config_NetworkConfig_ProtocolFlags,
|
||||||
|
formatEnumName: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "NTP Config",
|
label: "NTP Config",
|
||||||
description: "NTP configuration",
|
description: "NTP configuration",
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
export function convertIntToIpAddress(int: number): string {
|
export function convertIntToIpAddress(int: number): string {
|
||||||
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${
|
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff
|
||||||
(int >> 24) & 0xff
|
}`;
|
||||||
}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertIpAddressToInt(ip: string): number | null {
|
export function convertIpAddressToInt(ip: string): number | undefined {
|
||||||
|
if (!ip) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
ip
|
ip
|
||||||
.split(".")
|
.split(".")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
|
|||||||
import { Device } from "../../components/PageComponents/Config/Device/index.tsx";
|
import { Device } from "../../components/PageComponents/Config/Device/index.tsx";
|
||||||
import { Display } from "@components/PageComponents/Config/Display.tsx";
|
import { Display } from "@components/PageComponents/Config/Display.tsx";
|
||||||
import { LoRa } from "@components/PageComponents/Config/LoRa.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 { Position } from "@components/PageComponents/Config/Position.tsx";
|
||||||
import { Power } from "@components/PageComponents/Config/Power.tsx";
|
import { Power } from "@components/PageComponents/Config/Power.tsx";
|
||||||
import { Security } from "../../components/PageComponents/Config/Security/Security.tsx";
|
import { Security } from "../../components/PageComponents/Config/Security/Security.tsx";
|
||||||
@@ -31,7 +31,6 @@ export const DeviceConfig = () => {
|
|||||||
{
|
{
|
||||||
label: "Network",
|
label: "Network",
|
||||||
element: Network,
|
element: Network,
|
||||||
// disabled: !metadata.get(0)?.hasWifi,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Display",
|
label: "Display",
|
||||||
|
|||||||
@@ -1,61 +1,27 @@
|
|||||||
import type { Message } from "@bufbuild/protobuf";
|
import { z } from "zod";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import {
|
|
||||||
IsBoolean,
|
|
||||||
IsEnum,
|
|
||||||
IsIP,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
Length,
|
|
||||||
} from "class-validator";
|
|
||||||
|
|
||||||
export class NetworkValidation
|
const AddressModeEnum = z.nativeEnum(Protobuf.Config.Config_NetworkConfig_AddressMode);
|
||||||
implements
|
const ProtocolFlagsEnum = z.nativeEnum(Protobuf.Config.Config_NetworkConfig_ProtocolFlags);
|
||||||
Omit<Protobuf.Config.Config_NetworkConfig, keyof Message | "ipv4Config"> {
|
|
||||||
@IsBoolean()
|
|
||||||
wifiEnabled: boolean;
|
|
||||||
|
|
||||||
@Length(1, 33)
|
export const NetworkValidationIpV4ConfigSchema = z.object({
|
||||||
@IsOptional({})
|
ip: z.string().ip(),
|
||||||
wifiSsid: string;
|
gateway: z.string().ip(),
|
||||||
|
subnet: z.string().ip(),
|
||||||
|
dns: z.string().ip(),
|
||||||
|
});
|
||||||
|
|
||||||
@Length(8, 64)
|
export const NetworkValidationSchema = z.object({
|
||||||
@IsOptional()
|
wifiEnabled: z.boolean(),
|
||||||
wifiPsk: string;
|
wifiSsid: z.string().min(0).max(33).optional(),
|
||||||
|
wifiPsk: z.string().min(0).max(64).optional(),
|
||||||
|
ntpServer: z.string().min(2).max(30),
|
||||||
|
ethEnabled: z.boolean(),
|
||||||
|
addressMode: AddressModeEnum,
|
||||||
|
ipv4Config: NetworkValidationIpV4ConfigSchema.optional(),
|
||||||
|
enabledProtocols: ProtocolFlagsEnum,
|
||||||
|
rsyslogServer: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
@Length(2, 30)
|
export type NetworkValidation = z.infer<typeof NetworkValidationSchema>;
|
||||||
ntpServer: string;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
ethEnabled: boolean;
|
|
||||||
|
|
||||||
@IsEnum(Protobuf.Config.Config_NetworkConfig_AddressMode)
|
|
||||||
addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode;
|
|
||||||
|
|
||||||
ipv4Config: NetworkValidationIpV4Config;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
rsyslogServer: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NetworkValidationIpV4Config implements
|
|
||||||
Omit<
|
|
||||||
Protobuf.Config.Config_NetworkConfig_IpV4Config,
|
|
||||||
keyof Message | "ip" | "gateway" | "subnet" | "dns"
|
|
||||||
> {
|
|
||||||
@IsIP()
|
|
||||||
@IsOptional()
|
|
||||||
ip: string;
|
|
||||||
|
|
||||||
@IsIP()
|
|
||||||
@IsOptional()
|
|
||||||
gateway: string;
|
|
||||||
|
|
||||||
@IsIP()
|
|
||||||
@IsOptional()
|
|
||||||
subnet: string;
|
|
||||||
|
|
||||||
@IsIP()
|
|
||||||
@IsOptional()
|
|
||||||
dns: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ZodError, ZodSchema } from "zod";
|
||||||
|
|
||||||
|
export function validateSchema<T>(
|
||||||
|
schema: ZodSchema<T>,
|
||||||
|
data: unknown
|
||||||
|
): { success: true; data: T } | { success: false; errors: ZodError["issues"] } {
|
||||||
|
const result = schema.safeParse(data);
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: result.data };
|
||||||
|
} else {
|
||||||
|
return { success: false, errors: result.error.issues };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user