mirror of
https://github.com/meshtastic/web.git
synced 2026-08-01 23:47:23 -04:00
Merge branch 'main' into dependabot/npm_and_yarn/tanstack-4e0cd3caa5
This commit is contained in:
@@ -68,7 +68,7 @@
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"base64-js": "^1.5.1",
|
||||
"better-result": "^2.9.2",
|
||||
"better-result": "^2.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -77,7 +77,7 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^4.0.0",
|
||||
"idb-keyval": "^6.3.0",
|
||||
"immer": "^11.1.11",
|
||||
"immer": "^11.1.15",
|
||||
"js-cookie": "^3.0.8",
|
||||
"lucide-react": "^1.24.0",
|
||||
"maplibre-gl": "5.24.0",
|
||||
@@ -85,7 +85,7 @@
|
||||
"react-dom": "^19.2.7",
|
||||
"react-error-boundary": "^6.1.2",
|
||||
"react-hook-form": "^7.81.0",
|
||||
"react-i18next": "^17.0.9",
|
||||
"react-i18next": "^17.0.10",
|
||||
"react-map-gl": "8.1.1",
|
||||
"react-qrcode-logo": "^4.1.0",
|
||||
"rfc4648": "^1.5.4",
|
||||
@@ -112,10 +112,10 @@
|
||||
"autoprefixer": "^10.5.2",
|
||||
"gzipper": "^8.3.0",
|
||||
"happy-dom": "^20.10.6",
|
||||
"oxfmt": "^0.58.0",
|
||||
"oxfmt": "^0.59.0",
|
||||
"oxlint": "^1.74.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tar": "^7.5.20",
|
||||
"testing-library": "^0.0.2",
|
||||
|
||||
@@ -249,7 +249,9 @@ export function useConnections() {
|
||||
// Read from the live store, not the memoized `connections` closure: callers
|
||||
// such as addConnectionAndConnect() add a connection and connect to it in the
|
||||
// same tick, before this hook re-renders, so the closure would be stale.
|
||||
const conn = useDeviceStore.getState().savedConnections.find((c) => c.id === id);
|
||||
const conn = useDeviceStore
|
||||
.getState()
|
||||
.savedConnections.find((c) => c.id === id);
|
||||
if (!conn) {
|
||||
log.warn("connect: unknown connection id", { id });
|
||||
return false;
|
||||
|
||||
@@ -6,7 +6,8 @@ import path from "node:path";
|
||||
* that talks to the non-browser node over the TCP phone API. It sends text,
|
||||
* blocks until a specific text is received, or reports the node's number.
|
||||
*/
|
||||
const PYTHON = process.env.E2E_PEER_PYTHON ?? path.resolve("e2e/peer/.venv/bin/python");
|
||||
const PYTHON =
|
||||
process.env.E2E_PEER_PYTHON ?? path.resolve("e2e/peer/.venv/bin/python");
|
||||
const SCRIPT = path.resolve("e2e/peer/peer.py");
|
||||
const HOST = process.env.E2E_PEER_HOST ?? "127.0.0.1";
|
||||
const PORT = process.env.E2E_PEER_PORT ?? "14404";
|
||||
@@ -16,7 +17,10 @@ function spawnPeer(args: string[]): ChildProcess {
|
||||
}
|
||||
|
||||
/** Invoke `onLine` for each complete stdout line. */
|
||||
function onStdoutLines(child: ChildProcess, onLine: (line: string) => void): void {
|
||||
function onStdoutLines(
|
||||
child: ChildProcess,
|
||||
onLine: (line: string) => void,
|
||||
): void {
|
||||
let buf = "";
|
||||
child.stdout?.on("data", (chunk: Buffer) => {
|
||||
buf += chunk.toString();
|
||||
@@ -44,7 +48,9 @@ export function peerSend(
|
||||
});
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
child.on("exit", (code) =>
|
||||
code === 0 ? resolve() : reject(new Error(`peer send exited ${code}: ${stderr.trim()}`)),
|
||||
code === 0
|
||||
? resolve()
|
||||
: reject(new Error(`peer send exited ${code}: ${stderr.trim()}`)),
|
||||
);
|
||||
child.on("error", reject);
|
||||
});
|
||||
|
||||
@@ -44,11 +44,17 @@ async function waitForHttps(url: string, timeoutMs: number): Promise<void> {
|
||||
if (ok) return;
|
||||
await sleep(1000);
|
||||
}
|
||||
throw new Error(`device webserver not ready at ${url} within ${timeoutMs}ms (last: ${lastErr})`);
|
||||
throw new Error(
|
||||
`device webserver not ready at ${url} within ${timeoutMs}ms (last: ${lastErr})`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Poll a TCP port until it accepts a connection. */
|
||||
async function waitForTcp(host: string, port: number, timeoutMs: number): Promise<void> {
|
||||
async function waitForTcp(
|
||||
host: string,
|
||||
port: number,
|
||||
timeoutMs: number,
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastErr = "connection refused";
|
||||
while (Date.now() < deadline) {
|
||||
@@ -83,7 +89,9 @@ export default async function globalSetup(): Promise<void> {
|
||||
console.log("[e2e] bringing up meshtasticd two-node mesh ...");
|
||||
execSync(`docker compose -f ${COMPOSE_FILE} up -d`, { stdio: "inherit" });
|
||||
} else {
|
||||
console.log(`[e2e] hardware mode: device=${NODE_A_URL} peer=${PEER_HOST}:${PEER_PORT}`);
|
||||
console.log(
|
||||
`[e2e] hardware mode: device=${NODE_A_URL} peer=${PEER_HOST}:${PEER_PORT}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[e2e] waiting for device webserver ${NODE_A_URL} ...`);
|
||||
|
||||
@@ -17,6 +17,8 @@ export default async function globalTeardown(): Promise<void> {
|
||||
console.log("[e2e] tearing down meshtasticd mesh ...");
|
||||
execSync(`docker compose -f ${COMPOSE_FILE} down -v`, { stdio: "inherit" });
|
||||
} else {
|
||||
console.log("[e2e] leaving meshtasticd mesh running (set E2E_DOCKER_DOWN=1 to stop).");
|
||||
console.log(
|
||||
"[e2e] leaving meshtasticd mesh running (set E2E_DOCKER_DOWN=1 to stop).",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import { expect, type Page } from "@playwright/test";
|
||||
export class ConnectionPage {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async connectHttp(opts: { host: string; tls: boolean; name?: string }): Promise<void> {
|
||||
async connectHttp(opts: {
|
||||
host: string;
|
||||
tls: boolean;
|
||||
name?: string;
|
||||
}): Promise<void> {
|
||||
const { host, tls, name = "E2E Device" } = opts;
|
||||
const page = this.page;
|
||||
|
||||
@@ -25,7 +29,8 @@ export class ConnectionPage {
|
||||
await dialog.locator("#url").fill(host);
|
||||
|
||||
const httpsSwitch = dialog.getByRole("switch");
|
||||
const isChecked = (await httpsSwitch.getAttribute("aria-checked")) === "true";
|
||||
const isChecked =
|
||||
(await httpsSwitch.getAttribute("aria-checked")) === "true";
|
||||
if (tls !== isChecked) {
|
||||
await httpsSwitch.click();
|
||||
}
|
||||
@@ -33,11 +38,16 @@ export class ConnectionPage {
|
||||
await dialog.getByRole("button", { name: "Test connection" }).click();
|
||||
|
||||
const save = dialog.getByRole("button", { name: "Save connection" });
|
||||
await expect(save, "Save enables only after the device is reachable").toBeEnabled({
|
||||
await expect(
|
||||
save,
|
||||
"Save enables only after the device is reachable",
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
});
|
||||
await save.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/messages\/broadcast\/0/, { timeout: 60_000 });
|
||||
await expect(page).toHaveURL(/\/messages\/broadcast\/0/, {
|
||||
timeout: 60_000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ export class MessagesPage {
|
||||
await expect(this.input()).toBeEnabled();
|
||||
// The SDK chat client lags the composer (device handshake + sqlocal init), so
|
||||
// an immediate send is silently dropped. Gate on the "Connected" status.
|
||||
await expect(this.page.getByText("Connected", { exact: true }).first()).toBeVisible({
|
||||
await expect(
|
||||
this.page.getByText("Connected", { exact: true }).first(),
|
||||
).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,13 +13,17 @@ test.describe("broadcast messaging over a real two-node mesh", () => {
|
||||
await messagesPage.waitReady();
|
||||
});
|
||||
|
||||
test("renders a broadcast received from a mesh peer (mesh -> web)", async ({ messagesPage }) => {
|
||||
test("renders a broadcast received from a mesh peer (mesh -> web)", async ({
|
||||
messagesPage,
|
||||
}) => {
|
||||
const nonce = `pong-${Date.now()}`;
|
||||
await peerSend(nonce);
|
||||
await messagesPage.expectMessage(nonce);
|
||||
});
|
||||
|
||||
test("delivers a typed broadcast to the mesh (web -> mesh)", async ({ messagesPage }) => {
|
||||
test("delivers a typed broadcast to the mesh (web -> mesh)", async ({
|
||||
messagesPage,
|
||||
}) => {
|
||||
const nonce = `ping-${Date.now()}`;
|
||||
// Listen on the peer node before sending, then type+send from the browser.
|
||||
const recv = await startPeerRecv(nonce, { timeout: 60 });
|
||||
|
||||
@@ -28,16 +28,15 @@ test.describe("direct messaging over a real two-node mesh", () => {
|
||||
await messagesPage.waitReady();
|
||||
});
|
||||
|
||||
test.fixme(
|
||||
"delivers a direct message from the browser to the peer node (web -> mesh)",
|
||||
async ({ messagesPage }) => {
|
||||
const peerNum = await peerNodeNum();
|
||||
const nonce = `dm-${Date.now()}`;
|
||||
const recv = await startPeerRecv(nonce, { timeout: 60 });
|
||||
await messagesPage.openDirectMessageByNodeNum(peerNum);
|
||||
await messagesPage.send(nonce);
|
||||
const from = await recv.received;
|
||||
expect(from).toBeGreaterThan(0);
|
||||
},
|
||||
);
|
||||
test.fixme("delivers a direct message from the browser to the peer node (web -> mesh)", async ({
|
||||
messagesPage,
|
||||
}) => {
|
||||
const peerNum = await peerNodeNum();
|
||||
const nonce = `dm-${Date.now()}`;
|
||||
const recv = await startPeerRecv(nonce, { timeout: 60 });
|
||||
await messagesPage.openDirectMessageByNodeNum(peerNum);
|
||||
await messagesPage.send(nonce);
|
||||
const from = await recv.received;
|
||||
expect(from).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
"@typescript/typescript6": "^6.0.2",
|
||||
"husky": "^9.1.0",
|
||||
"lint-staged": "^17.0.8",
|
||||
"oxfmt": "^0.58.0",
|
||||
"oxfmt": "^0.59.0",
|
||||
"oxlint": "^1.74.0",
|
||||
"tsdown": "^0.22.7",
|
||||
"tsdown": "^0.22.11",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@meshtastic/sdk": "workspace:*",
|
||||
"better-result": "^2.9.2"
|
||||
"better-result": "^2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.0.0",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"@bufbuild/protobuf": "^2.12.1",
|
||||
"@meshtastic/protobufs": "jsr:^2.7.18",
|
||||
"@preact/signals-core": "^1.14.4",
|
||||
"better-result": "^2.9.2",
|
||||
"better-result": "^2.10.0",
|
||||
"crc": "npm:crc@^4.3.2",
|
||||
"ste-simple-events": "^3.0.11",
|
||||
"tslog": "^4.11.0"
|
||||
|
||||
@@ -33,7 +33,9 @@ describe("NodesClient", () => {
|
||||
const { transport } = createFakeTransport();
|
||||
const client = new MeshClient({ transport });
|
||||
|
||||
client.events.onNodeInfoPacket.dispatch(create(Protobuf.Mesh.NodeInfoSchema, { num: 100 }));
|
||||
client.events.onNodeInfoPacket.dispatch(
|
||||
create(Protobuf.Mesh.NodeInfoSchema, { num: 100 }),
|
||||
);
|
||||
client.events.onTelemetryPacket.dispatch({
|
||||
id: 1,
|
||||
from: 100,
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@meshtastic/sdk": "workspace:*",
|
||||
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7",
|
||||
"better-result": "^2.9.2"
|
||||
"better-result": "^2.10.0"
|
||||
},
|
||||
"tsdown": {
|
||||
"clean": true,
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
"tailwind-merge": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.2",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"publint": "^0.3.21",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.1.4",
|
||||
|
||||
9
packages/ui/vitest.config.ts
Normal file
9
packages/ui/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineProject } from "vitest/config";
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: "@meshtastic/ui",
|
||||
environment: "happy-dom",
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
},
|
||||
});
|
||||
@@ -24,7 +24,10 @@ export default defineConfig({
|
||||
timeout: 90_000,
|
||||
expect: { timeout: 15_000 },
|
||||
|
||||
reporter: [["list"], ["html", { outputFolder: "e2e/.report", open: "never" }]],
|
||||
reporter: [
|
||||
["list"],
|
||||
["html", { outputFolder: "e2e/.report", open: "never" }],
|
||||
],
|
||||
|
||||
use: {
|
||||
baseURL: `http://localhost:${WEB_PORT}`,
|
||||
|
||||
1093
pnpm-lock.yaml
generated
1093
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user