Merge branch 'main' into dependabot/npm_and_yarn/tanstack-4e0cd3caa5

This commit is contained in:
Hunter Thornsberry
2026-07-22 12:51:32 -04:00
committed by GitHub
18 changed files with 867 additions and 355 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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} ...`);

View File

@@ -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).",
);
}
}

View File

@@ -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,
});
}
}

View File

@@ -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,
});
}

View File

@@ -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 });

View File

@@ -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);
});
});

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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",

View 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"],
},
});

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff