build: toolchain and dependency upgrades (TypeScript 6, Node 24 CI, Vite 8, Vitest 4, Go/Rust deps) (#1401)

* build(client): upgrade TypeScript to 6.0.3

Staging step toward TypeScript 7 (the native compiler), which needs its
7.1 stable API before typescript-eslint and Stryker's typescript-checker
can run on it. TS 6 is the JS-based bridge release that aligns config
defaults with 7.

Two fallout fixes:
- tsconfig.e2e.json: TS 6 defaults "types" to [] instead of every
  installed @types package, so the Playwright layer's Node globals
  (process, Buffer) need an explicit "types": ["node"].
- media-visibility.test.ts: TS 6's DOM lib adds scrollMargin to
  IntersectionObserver, so the mock grows the property.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lw5KEz6gdD816Wxmm4A3Bn

* build(server): bump chi to 5.3.2, modernc.org/sqlite to 1.57.0, toolchain to go1.26.7

Go 1.27 deliberately deferred until 1.27.1 lands.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lw5KEz6gdD816Wxmm4A3Bn

* build(tauri): bump tokio-tungstenite to 0.30, refresh Cargo.lock

In-range lockfile refresh via cargo update; tungstenite 0.29/0.30 changes
are client-API-neutral (header handling, server-side handshake hardening).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lw5KEz6gdD816Wxmm4A3Bn

* build(client): upgrade vite 8, vitest 4, jsdom 30, stryker 10 + minors

- vite 6 -> 8: Rolldown requires the function form of manualChunks;
  __dirname -> import.meta.dirname in configs
- vitest 3 -> 4: browser provider moved to @vitest/browser-playwright;
  vi.fn() mocks now need explicit signatures (typed throughout tests);
  constructor mocks use function impls; restoreAllMocks no longer resets
  vi.fn state; matchMedia spies replaced with vi.stubGlobal
- jsdom 29 -> 30: one internal bookkeeping abort listener per signal,
  listener-count regression tests adjusted (leak detection retained)
- stryker 9 -> 10, @types/node 20 -> 24, eslint/oxlint/livekit-client minors
- tsconfigs: explicit "types" now that TS6/vitest4 stop injecting
  @types/node ambiently; build config keeps Node globals out of src/

Validated: tsc (main/build/e2e), eslint, oxlint, knip, prettier,
unit+integration (5196 tests), browser suite, vite build, stryker dry run.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lw5KEz6gdD816Wxmm4A3Bn

* ci: move Node 20 (EOL 2026-04-30) to Node 24 LTS

The jsdom suite runs on modern Node without --no-experimental-webstorage:
tests/setup.ts already replaces the shadowed localStorage with an
in-memory shim. Client CLAUDE.md gotcha updated accordingly.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lw5KEz6gdD816Wxmm4A3Bn

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-08-22 06:48:58 +02:00
committed by GitHub
co-authored by Claude
parent c22bc14946
commit 9df0e63b5f
45 changed files with 2579 additions and 3206 deletions
+6 -6
View File
@@ -125,7 +125,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -183,7 +183,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -270,7 +270,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -329,7 +329,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -371,7 +371,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -463,7 +463,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
+4 -4
View File
@@ -54,7 +54,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -106,7 +106,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -296,7 +296,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
@@ -463,7 +463,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: Client/tauri-client/package-lock.json
+4 -4
View File
@@ -14,10 +14,10 @@ Rust backend in `src-tauri/` for native APIs only. LiveKit handles voice/video.
## Gotchas
- **On Node 22+ you must run `NODE_OPTIONS=--no-experimental-webstorage npm test`.**
Native Web Storage shadows jsdom's `localStorage` and fails ~478 tests that
have nothing to do with your change. That is a local toolchain artifact, not
a regression — do not "fix" those failures. CI pins Node 20.
- Node's native Web Storage (Node 22+) shadows jsdom's `localStorage`;
`tests/setup.ts` replaces it with an in-memory shim, so the suite runs on
modern Node without `--no-experimental-webstorage`. If storage tests fail
en masse, suspect that shim before your change. CI pins Node 24.
- `src/lib/dispatcher.ts` is the single WS-event entry point **into the
stores**: server events reach domain stores only through a `ws.on(...)`
subscription registered there. Other modules do register their own
+1319 -1840
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -34,24 +34,24 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1",
"@stryker-mutator/api": "^9.6.1",
"@stryker-mutator/core": "^9.6.1",
"@stryker-mutator/typescript-checker": "^9.6.1",
"@stryker-mutator/vitest-runner": "^9.6.1",
"@stryker-mutator/api": "^10.0.0",
"@stryker-mutator/core": "^10.0.0",
"@stryker-mutator/typescript-checker": "^10.0.0",
"@stryker-mutator/vitest-runner": "^10.0.0",
"@tauri-apps/cli": "^2",
"@types/node": "^20.19.43",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3",
"eslint": "^10.8.1",
"@types/node": "^24.13.3",
"@vitest/browser-playwright": "^4.1.11",
"@vitest/coverage-v8": "^4.1.11",
"eslint": "^10.9.0",
"fast-check": "^4.9.0",
"jsdom": "^29.1.1",
"jsdom": "^30.0.1",
"knip": "^6.32.2",
"oxlint": "^1.78.0",
"oxlint": "^1.79.0",
"prettier": "^3.9.6",
"typescript": "^5.7",
"typescript": "^6.0.3",
"typescript-eslint": "^8.67.0",
"vite": "^6",
"vitest": "^3"
"vite": "^8.2.2",
"vitest": "^4.1.11"
},
"prettier": {
"singleQuote": false,
@@ -73,7 +73,7 @@
"@tauri-apps/plugin-notification": "^2",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-process": "^2.3.1",
"livekit-client": "^2.21.0"
"livekit-client": "^2.22.0"
},
"overrides": {
"qs": "^6.15.3",
+776 -1117
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -47,7 +47,7 @@ tauri-plugin-fs = "2"
tauri-plugin-updater = "2.10"
tauri-plugin-process = "2"
url = "2"
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] }
tokio-tungstenite = { version = "0.30.0", features = ["rustls-tls-webpki-roots"] }
futures-util = "0.3.32"
tokio = { version = "1", features = ["sync", "net", "io-util", "rt", "macros"] }
tokio-rustls = { version = "0.26", default-features = false }
@@ -32,7 +32,9 @@ describe("AccessibilityTab — reduced-motion arbitration (OC-0232)", () => {
beforeEach(() => {
matchMediaListeners = new Map();
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => {
// jsdom does not implement matchMedia, and vitest 4's spyOn refuses to spy
// on undefined, so stub the global instead.
vi.stubGlobal("matchMedia", (query: string) => {
const mql = {
matches: matchMediaMatches,
media: query,
@@ -62,6 +64,7 @@ describe("AccessibilityTab — reduced-motion arbitration (OC-0232)", () => {
document.documentElement.classList.remove("reduced-motion");
localStorage.clear();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("keeps reduced-motion applied when OS sync is on and the OS still prefers it, even after a manual Reduce Motion toggle is switched off", () => {
+4 -4
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
// Mock the Tauri HTTP plugin — vi.hoisted ensures the fn is available when
// vi.mock's factory runs (hoisted above all imports).
@@ -19,7 +19,7 @@ vi.mock("../../src/lib/httpProxy", () => ({
stopHttpProxy: () => Promise.resolve(),
}));
import { createApiClient, ApiClientError } from "../../src/lib/api";
import { createApiClient, ApiClientError, type OnUnauthorized } from "../../src/lib/api";
function jsonResponse(data: unknown, status = 200): Response {
return {
@@ -54,11 +54,11 @@ function brokenJsonErrorResponse(status: number, statusText: string): Response {
describe("API Client", () => {
let api: ReturnType<typeof createApiClient>;
let onUnauthorized: ReturnType<typeof vi.fn>;
let onUnauthorized: Mock<OnUnauthorized>;
beforeEach(() => {
mockFetch.mockReset();
onUnauthorized = vi.fn();
onUnauthorized = vi.fn<OnUnauthorized>();
api = createApiClient({ host: "localhost:8443", token: "test-token" }, onUnauthorized);
});
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const { fetchMock, putSpy } = vi.hoisted(() => ({
fetchMock: vi.fn<any>(),
putSpy: vi.fn<any>(),
putSpy: vi.fn<(value: string, key: string) => void>(),
}));
vi.mock("@tauri-apps/plugin-http", () => ({
@@ -279,10 +279,17 @@ describe("AudioPipeline", () => {
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockRoom = {
@@ -458,7 +465,7 @@ describe("AudioPipeline", () => {
it("handles AudioContext constructor failure gracefully", () => {
vi.stubGlobal(
"AudioContext",
vi.fn(() => {
vi.fn(function () {
throw new Error("AudioContext not supported");
}),
);
@@ -591,10 +598,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation((tracks: unknown) => ({ tracks })),
vi.fn(function (tracks: unknown) {
return { tracks };
}),
);
return { mockSender };
}
@@ -676,7 +690,10 @@ describe("AudioPipeline", () => {
});
it("creates AudioContext with sampleRate 48000", () => {
const AudioContextSpy = vi.fn().mockReturnValue({
const AudioContextSpy = vi.fn(function () {
return audioCtxForSpy;
});
const audioCtxForSpy = {
resume: vi.fn().mockResolvedValue(undefined),
createMediaStreamSource: vi.fn().mockReturnValue({ connect: vi.fn() }),
createAnalyser: vi.fn().mockReturnValue({
@@ -699,11 +716,13 @@ describe("AudioPipeline", () => {
close: vi.fn().mockResolvedValue(undefined),
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
});
};
vi.stubGlobal("AudioContext", AudioContextSpy);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
const mockRoom = {
@@ -749,10 +768,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
const mockRoom = {
@@ -798,10 +824,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
const mockRoom = {
@@ -846,10 +879,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
const mockRoom = {
@@ -911,10 +951,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
const mockRoom = {
@@ -968,10 +1015,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
const mockRoom = {
@@ -1046,7 +1100,9 @@ describe("AudioPipeline", () => {
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
pipeline.setRoom(mockRoom);
@@ -1114,7 +1170,9 @@ describe("AudioPipeline", () => {
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
pipeline.setRoom(mockRoom);
@@ -234,10 +234,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
});
@@ -88,10 +88,17 @@ describe("AudioPipeline", () => {
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -164,10 +171,17 @@ describe("AudioPipeline", () => {
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -246,10 +260,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -340,10 +361,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -412,10 +440,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -480,10 +515,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -559,10 +601,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -81,19 +81,28 @@ describe("AudioPipeline VAD worklet teardown (OC-0231)", () => {
vi.stubGlobal(
"AudioWorkletNode",
vi.fn().mockImplementation(() => ({
port: {
postMessage: vi.fn(),
onmessage: null as ((event: MessageEvent) => void) | null,
},
connect: vi.fn(),
disconnect: vi.fn(),
})),
vi.fn(function () {
return {
port: {
postMessage: vi.fn(),
onmessage: null as ((event: MessageEvent) => void) | null,
},
connect: vi.fn(),
disconnect: vi.fn(),
};
}),
);
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockRoom = {
@@ -123,19 +123,28 @@ describe("AudioPipeline", () => {
// Mock AudioWorkletNode
vi.stubGlobal(
"AudioWorkletNode",
vi.fn().mockImplementation(() => ({
port: {
postMessage: vi.fn(),
onmessage: null as ((event: MessageEvent) => void) | null,
},
connect: vi.fn(),
disconnect: vi.fn(),
})),
vi.fn(function () {
return {
port: {
postMessage: vi.fn(),
onmessage: null as ((event: MessageEvent) => void) | null,
},
connect: vi.fn(),
disconnect: vi.fn(),
};
}),
);
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockRoom = {
@@ -297,10 +306,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -408,18 +424,27 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockReturnValue(addModulePromise) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
vi.stubGlobal(
"AudioWorkletNode",
vi.fn().mockImplementation(() => ({
port: { postMessage: vi.fn(), onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
})),
vi.fn(function () {
return {
port: { postMessage: vi.fn(), onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -493,18 +518,27 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockReturnValue(addModulePromise) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
vi.stubGlobal(
"AudioWorkletNode",
vi.fn().mockImplementation(() => ({
port: { postMessage: vi.fn(), onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
})),
vi.fn(function () {
return {
port: { postMessage: vi.fn(), onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -577,18 +611,27 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockResolvedValue(undefined) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
vi.stubGlobal(
"AudioWorkletNode",
vi.fn().mockImplementation(() => ({
port: { postMessage: vi.fn(), onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
})),
vi.fn(function () {
return {
port: { postMessage: vi.fn(), onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -668,18 +711,27 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockResolvedValue(undefined) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
vi.stubGlobal(
"AudioWorkletNode",
vi.fn().mockImplementation(() => ({
port: { postMessage: postMessageSpy, onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
})),
vi.fn(function () {
return {
port: { postMessage: postMessageSpy, onmessage: null },
connect: vi.fn(),
disconnect: vi.fn(),
};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -747,10 +799,17 @@ describe("AudioPipeline", () => {
state: "running",
audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no")) },
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
vi.stubGlobal(
"AudioContext",
vi.fn(function () {
return mockAudioCtx;
}),
);
vi.stubGlobal(
"MediaStream",
vi.fn().mockImplementation(() => ({})),
vi.fn(function () {
return {};
}),
);
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from "vitest";
// Mock livekitSession (required by streamPreview). rePinPeerIdentity is hoisted
// so the mock factory can reference it and tests can assert re-pin was invoked.
@@ -183,15 +183,15 @@ function setAdminUser(): void {
describe("ChannelSidebar", () => {
let container: HTMLDivElement;
let sidebar: ReturnType<typeof createChannelSidebar>;
let onVoiceJoin: ReturnType<typeof vi.fn>;
let onVoiceLeave: ReturnType<typeof vi.fn>;
let onVoiceJoin: Mock<(channelId: number) => void>;
let onVoiceLeave: Mock<() => void>;
beforeEach(() => {
resetStores();
container = document.createElement("div");
document.body.appendChild(container);
onVoiceJoin = vi.fn();
onVoiceLeave = vi.fn();
onVoiceJoin = vi.fn<(channelId: number) => void>();
onVoiceLeave = vi.fn<() => void>();
sidebar = createChannelSidebar({ onVoiceJoin, onVoiceLeave });
});
@@ -2247,15 +2247,15 @@ describe("ChannelSidebar voice identity badge", () => {
describe("ChannelSidebar channel feature flags", () => {
let container: HTMLDivElement;
let sidebar: ReturnType<typeof createChannelSidebar>;
let onVoiceJoin: ReturnType<typeof vi.fn>;
let onVoiceLeave: ReturnType<typeof vi.fn>;
let onVoiceJoin: Mock<(channelId: number) => void>;
let onVoiceLeave: Mock<() => void>;
beforeEach(() => {
resetStores();
container = document.createElement("div");
document.body.appendChild(container);
onVoiceJoin = vi.fn();
onVoiceLeave = vi.fn();
onVoiceJoin = vi.fn<(channelId: number) => void>();
onVoiceLeave = vi.fn<() => void>();
sidebar = createChannelSidebar({ onVoiceJoin, onVoiceLeave });
});
@@ -247,7 +247,7 @@ describe("CreateChannelModal", () => {
it("disables submit button and shows 'Creating...' while creating", async () => {
let resolveCreate: (() => void) | undefined;
const onCreate = vi.fn<any>(
const onCreate = vi.fn<NonNullable<CreateChannelModalOptions["onCreate"]>>(
() =>
new Promise<void>((resolve) => {
resolveCreate = resolve;
@@ -148,7 +148,7 @@ describe("DeleteChannelModal", () => {
it("disables button and shows 'Deleting...' during delete", async () => {
let resolveDelete: (() => void) | undefined;
const onConfirm = vi.fn<any>(
const onConfirm = vi.fn<() => Promise<void>>(
() =>
new Promise<void>((resolve) => {
resolveDelete = resolve;
@@ -8,7 +8,7 @@
* channels for every member of the server.
*/
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
import { beforeEach, afterEach, describe, expect, it, vi, type Mock } from "vitest";
import {
attachDragHandlers,
@@ -60,7 +60,7 @@ interface Rig {
container: HTMLElement;
items: Map<number, HTMLElement>;
channels: Channel[];
onReorder: ReturnType<typeof vi.fn>;
onReorder: Mock<(reorders: readonly ChannelReorderData[]) => void>;
abort: AbortController;
}
@@ -105,7 +105,7 @@ function buildRig(channels: Channel[]): Rig {
document.body.appendChild(container);
const items = new Map<number, HTMLElement>();
const onReorder = vi.fn();
const onReorder = vi.fn<(reorders: readonly ChannelReorderData[]) => void>();
const abort = new AbortController();
rigAborts.push(abort);
@@ -259,7 +259,7 @@ describe("EditChannelModal", () => {
it("disables save button and shows 'Saving...' during save", async () => {
let resolveSave: (() => void) | undefined;
const onSave = vi.fn<any>(
const onSave = vi.fn<(data: EditChannelData) => Promise<void>>(
() =>
new Promise<void>((resolve) => {
resolveSave = resolve;
@@ -4,7 +4,7 @@
* how it shares the composer with the @-mention popup).
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from "vitest";
vi.mock("@lib/livekitSession", () => ({
leaveVoice: vi.fn(),
@@ -32,6 +32,7 @@ import {
MIN_EMOJI_QUERY,
} from "../../src/components/EmojiAutocomplete";
import { createMessageInput } from "../../src/components/MessageInput";
import type { MessageInputOptions } from "../../src/components/MessageInput";
import { emojiStore, setCustomEmoji, clearCustomEmoji } from "../../src/stores/emoji.store";
import { membersStore } from "../../src/stores/members.store";
@@ -336,12 +337,12 @@ describe("createEmojiAutocomplete combobox wiring", () => {
describe("composer :shortcode integration", () => {
let container: HTMLDivElement;
let input: ReturnType<typeof createMessageInput>;
let onSend: ReturnType<typeof vi.fn>;
let onSend: Mock<MessageInputOptions["onSend"]>;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
onSend = vi.fn();
onSend = vi.fn<MessageInputOptions["onSend"]>();
input = createMessageInput({
channelId: 1,
channelName: "general",
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
const { mockVoiceGetState } = vi.hoisted(() => ({
mockVoiceGetState: vi.fn(() => ({ currentChannelId: null as number | null })),
@@ -20,18 +20,18 @@ vi.mock("@stores/voice.store", () => ({
const { attachGlobalKeybinds } = await import("../../src/pages/main-page/GlobalKeybinds");
function makeHandlers(): {
onSearch: ReturnType<typeof vi.fn>;
onToggleMute: ReturnType<typeof vi.fn>;
onToggleDeafen: ReturnType<typeof vi.fn>;
onToggleCamera: ReturnType<typeof vi.fn>;
onUploadFile: ReturnType<typeof vi.fn>;
onSearch: Mock<() => void>;
onToggleMute: Mock<() => void>;
onToggleDeafen: Mock<() => void>;
onToggleCamera: Mock<() => void>;
onUploadFile: Mock<() => void>;
} {
return {
onSearch: vi.fn(),
onToggleMute: vi.fn(),
onToggleDeafen: vi.fn(),
onToggleCamera: vi.fn(),
onUploadFile: vi.fn(),
onSearch: vi.fn<() => void>(),
onToggleMute: vi.fn<() => void>(),
onToggleDeafen: vi.fn<() => void>(),
onToggleCamera: vi.fn<() => void>(),
onUploadFile: vi.fn<() => void>(),
};
}
@@ -9,10 +9,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
const mockSetKey = vi.hoisted(() => vi.fn());
vi.mock("livekit-client", () => ({
ExternalE2EEKeyProvider: vi.fn(() => ({
setKey: mockSetKey,
getKeys: vi.fn().mockReturnValue([]),
})),
// vitest 4 mocks honor construct semantics — `new` needs a real function, not an arrow.
ExternalE2EEKeyProvider: vi.fn(function () {
return {
setKey: mockSetKey,
getKeys: vi.fn().mockReturnValue([]),
};
}),
}));
const mockKeyPair = vi.hoisted(() => ({
@@ -47,7 +47,10 @@ const mockRoom = vi.hoisted(() => ({
}));
vi.mock("livekit-client", () => ({
Room: vi.fn(() => mockRoom),
// vitest 4 mocks honor construct semantics — `new` needs a real function, not an arrow.
Room: vi.fn(function () {
return mockRoom;
}),
RoomEvent: {
TrackSubscribed: "trackSubscribed",
TrackUnsubscribed: "trackUnsubscribed",
@@ -77,11 +80,14 @@ vi.mock("livekit-client", () => ({
h1080fps30: { resolution: { width: 1920, height: 1080 } },
},
DisconnectReason: { CLIENT_INITIATED: 0 },
ExternalE2EEKeyProvider: vi.fn(() => ({
setKey: vi.fn(),
getKeys: vi.fn().mockReturnValue([]),
removeAllListeners: vi.fn(),
})),
// vitest 4 mocks honor construct semantics — `new` needs a real function, not an arrow.
ExternalE2EEKeyProvider: vi.fn(function () {
return {
setKey: vi.fn(),
getKeys: vi.fn().mockReturnValue([]),
removeAllListeners: vi.fn(),
};
}),
createLocalVideoTrack: vi.fn(async () => ({
kind: "video",
mediaStreamTrack: new MediaStreamTrack(),
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
// clearAuth pulls in voice/livekit/notification teardown; stub it so this test
// stays a pure unit test of the best-effort revoke-then-teardown ordering.
@@ -9,7 +9,7 @@ vi.mock("@stores/auth.store", () => ({
import { logout } from "../../src/lib/logout";
function makeApi(logoutImpl: () => Promise<void>): { logout: ReturnType<typeof vi.fn> } {
function makeApi(logoutImpl: () => Promise<void>): { logout: Mock<() => Promise<void>> } {
return { logout: vi.fn(logoutImpl) };
}
@@ -45,7 +45,9 @@ describe("LogsTab", () => {
let controller: AbortController;
beforeEach(() => {
vi.restoreAllMocks();
// vitest 4's restoreAllMocks no longer resets vi.fn() state — reset
// explicitly so call counts don't accumulate across tests.
vi.resetAllMocks();
controller = new AbortController();
mockGetLogBuffer.mockReturnValue([]);
mockAddLogListener.mockReturnValue(() => {});
@@ -16,6 +16,7 @@ const disconnectMock = vi.fn();
class MockIntersectionObserver implements IntersectionObserver {
readonly root: Element | null = null;
readonly rootMargin: string = "0px";
readonly scrollMargin: string = "0px";
readonly thresholds: readonly number[] = [0];
constructor(callback: IntersectionObserverCallback) {
observerCallback = callback;
@@ -3,7 +3,7 @@
* entries, keyboard navigation, and composer integration.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from "vitest";
vi.mock("@lib/livekitSession", () => ({
leaveVoice: vi.fn(),
@@ -21,6 +21,7 @@ import {
MAX_MENTION_SUGGESTIONS,
} from "../../src/components/MentionAutocomplete";
import { createMessageInput } from "../../src/components/MessageInput";
import type { MessageInputOptions } from "../../src/components/MessageInput";
import { membersStore } from "../../src/stores/members.store";
import { authStore } from "../../src/stores/auth.store";
import { channelsStore, setRoles } from "../../src/stores/channels.store";
@@ -132,13 +133,13 @@ describe("filterMentionSuggestions", () => {
});
describe("createMentionAutocomplete", () => {
let onSelect: ReturnType<typeof vi.fn>;
let onClose: ReturnType<typeof vi.fn>;
let onSelect: Mock<(token: string) => void>;
let onClose: Mock<() => void>;
let popup: ReturnType<typeof createMentionAutocomplete>;
beforeEach(() => {
onSelect = vi.fn();
onClose = vi.fn();
onSelect = vi.fn<(token: string) => void>();
onClose = vi.fn<() => void>();
popup = createMentionAutocomplete({ onSelect, onClose });
document.body.appendChild(popup.element);
});
@@ -317,12 +318,12 @@ describe("createMentionAutocomplete combobox wiring", () => {
describe("composer integration", () => {
let container: HTMLDivElement;
let input: ReturnType<typeof createMessageInput>;
let onSend: ReturnType<typeof vi.fn>;
let onSend: Mock<MessageInputOptions["onSend"]>;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
onSend = vi.fn();
onSend = vi.fn<MessageInputOptions["onSend"]>();
input = createMessageInput({
channelId: 1,
channelName: "general",
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
// ---------------------------------------------------------------------------
// Mocks
@@ -72,11 +72,11 @@ function makeAbort(): { signal: AbortSignal; abort: () => void } {
// ---------------------------------------------------------------------------
describe("createMessageController", () => {
let showError: ReturnType<typeof vi.fn>;
let showError: Mock<(msg: string) => void>;
beforeEach(() => {
vi.clearAllMocks();
showError = vi.fn();
showError = vi.fn<(msg: string) => void>();
mockIsChannelLoaded.mockReturnValue(false);
mockGetChannelMessages.mockReturnValue([]);
});
@@ -9,22 +9,27 @@ describe("syncOsMotionListener", () => {
matchMediaListeners = new Map();
matchMediaMatches = false;
// Mock matchMedia to return a controllable MediaQueryList
vi.spyOn(window, "matchMedia").mockImplementation((_query: string) => {
const mql = {
matches: matchMediaMatches,
media: _query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn((type: string, handler: Function, _opts?: any) => {
matchMediaListeners.set(type, handler);
}),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(() => true),
} as unknown as MediaQueryList;
return mql;
});
// Mock matchMedia to return a controllable MediaQueryList. jsdom does not
// implement matchMedia, and vitest 4's spyOn refuses to spy on undefined,
// so stub the global instead.
vi.stubGlobal(
"matchMedia",
vi.fn((_query: string) => {
const mql = {
matches: matchMediaMatches,
media: _query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn((type: string, handler: Function, _opts?: any) => {
matchMediaListeners.set(type, handler);
}),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(() => true),
} as unknown as MediaQueryList;
return mql;
}),
);
// Clean up any leftover class/storage from previous tests
document.documentElement.classList.remove("reduced-motion");
@@ -35,6 +40,7 @@ describe("syncOsMotionListener", () => {
// Disable the listener to clean up internal state
syncOsMotionListener(false);
document.documentElement.classList.remove("reduced-motion");
vi.unstubAllGlobals();
});
it("adds reduced-motion class when OS prefers reduced motion", () => {
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
import { createQuickSwitcher } from "@components/QuickSwitcher";
import type { QuickSwitcherOptions } from "@components/QuickSwitcher";
import { channelsStore, setChannels } from "@stores/channels.store";
@@ -24,16 +24,16 @@ const testChannels: ReadyChannel[] = [
describe("QuickSwitcher", () => {
let container: HTMLDivElement;
let switcher: ReturnType<typeof createQuickSwitcher>;
let onSelectChannel: ReturnType<typeof vi.fn>;
let onClose: ReturnType<typeof vi.fn>;
let onSelectChannel: Mock<(channelId: number) => void>;
let onClose: Mock<() => void>;
beforeEach(() => {
resetStore();
setChannels(testChannels);
container = document.createElement("div");
document.body.appendChild(container);
onSelectChannel = vi.fn();
onClose = vi.fn();
onSelectChannel = vi.fn<(channelId: number) => void>();
onClose = vi.fn<() => void>();
switcher = createQuickSwitcher({ onSelectChannel, onClose });
});
@@ -334,8 +334,12 @@ describe("attachReactionTooltip", () => {
);
}
// Exactly two registrations regardless of chip count: the module's own
// guarded listener plus the single internal bookkeeping listener jsdom
// registers per signal for `{ signal }`-scoped DOM listeners. A per-chip
// leak would register one per chip (50+).
const abortRegistrations = addEventListenerSpy.mock.calls.filter(([type]) => type === "abort");
expect(abortRegistrations).toHaveLength(1);
expect(abortRegistrations).toHaveLength(2);
});
it("hides every currently-hovering chip on a signal when it aborts, not just the first attached", () => {
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
const mockRetryMicPermission = vi.fn().mockResolvedValue(undefined);
vi.mock("@lib/livekitSession", () => ({
@@ -101,11 +101,11 @@ describe("Screen Share Button in VoiceWidget", () => {
let container: HTMLDivElement;
let comp: ReturnType<typeof createVoiceWidget>;
let handlers: {
onDisconnect: ReturnType<typeof vi.fn>;
onMuteToggle: ReturnType<typeof vi.fn>;
onDeafenToggle: ReturnType<typeof vi.fn>;
onCameraToggle: ReturnType<typeof vi.fn>;
onScreenshareToggle: ReturnType<typeof vi.fn>;
onDisconnect: Mock<() => void>;
onMuteToggle: Mock<() => void>;
onDeafenToggle: Mock<() => void>;
onCameraToggle: Mock<() => void>;
onScreenshareToggle: Mock<() => void>;
};
beforeEach(() => {
@@ -113,11 +113,11 @@ describe("Screen Share Button in VoiceWidget", () => {
container = document.createElement("div");
document.body.appendChild(container);
handlers = {
onDisconnect: vi.fn(),
onMuteToggle: vi.fn(),
onDeafenToggle: vi.fn(),
onCameraToggle: vi.fn(),
onScreenshareToggle: vi.fn(),
onDisconnect: vi.fn<() => void>(),
onMuteToggle: vi.fn<() => void>(),
onDeafenToggle: vi.fn<() => void>(),
onCameraToggle: vi.fn<() => void>(),
onScreenshareToggle: vi.fn<() => void>(),
};
});
@@ -12,7 +12,7 @@
* test would sail straight past.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
import { Track } from "livekit-client";
import type { LocalTrack, LocalVideoTrack, Room } from "livekit-client";
import type { WsClient } from "@lib/ws";
@@ -114,16 +114,16 @@ function fakeDeps(
hasWs = true,
): VideoTrackDeps & {
wsSend: ReturnType<typeof vi.fn>;
onError: ReturnType<typeof vi.fn>;
reapplyAudioPipeline: ReturnType<typeof vi.fn>;
onError: Mock<(message: string) => void>;
reapplyAudioPipeline: Mock<() => void>;
} {
const wsSend = vi.fn();
const ws = hasWs ? ({ send: wsSend } as unknown as WsClient) : null;
return {
getRoom: () => room,
getWs: () => ws,
onError: vi.fn(),
reapplyAudioPipeline: vi.fn(),
onError: vi.fn<(message: string) => void>(),
reapplyAudioPipeline: vi.fn<() => void>(),
wsSend,
};
}
@@ -361,8 +361,12 @@ describe("streamPreview", () => {
// A re-render re-attaches the same row to the same sidebar-lifetime signal.
attachStreamPreview(row1, 1, "Alice", false, true, ac.signal);
// Exactly two registrations regardless of attach count: the module's own
// guarded listener plus the single internal bookkeeping listener jsdom
// registers per signal for `{ signal }`-scoped DOM listeners. A
// per-attach leak would register one per attach call.
const abortCalls = addSpy.mock.calls.filter(([type]) => type === "abort");
expect(abortCalls).toHaveLength(1);
expect(abortCalls).toHaveLength(2);
});
// v091: a structural re-render (clearChildren + rebuild) detaches the old
@@ -443,7 +443,7 @@ describe("TOTP Settings", () => {
// Simulate MainPage's onConfirmTotp: it calls updateUser after API success
const options = makeOptions({
onConfirmTotp: vi.fn().mockImplementation(async () => {
(updateUser as ReturnType<typeof vi.fn>)({ totp_enabled: true });
updateUser({ totp_enabled: true });
}),
});
const overlay = createSettingsOverlay(options);
@@ -659,7 +659,7 @@ describe("TOTP Settings", () => {
// Simulate MainPage's onDisableTotp: it calls updateUser after API success
const options = makeOptions({
onDisableTotp: vi.fn().mockImplementation(async () => {
(updateUser as ReturnType<typeof vi.fn>)({ totp_enabled: false });
updateUser({ totp_enabled: false });
}),
});
const overlay = createSettingsOverlay(options);
@@ -706,7 +706,7 @@ describe("TOTP Settings", () => {
const options = makeOptions({
onConfirmTotp: vi.fn().mockImplementation(async () => {
// MainPage calls updateUser here — this is the single owner
(updateUser as ReturnType<typeof vi.fn>)({ totp_enabled: true });
updateUser({ totp_enabled: true });
}),
});
const overlay = createSettingsOverlay(options);
@@ -751,7 +751,7 @@ describe("TOTP Settings", () => {
mockTotpEnabled = true;
const options = makeOptions({
onDisableTotp: vi.fn().mockImplementation(async () => {
(updateUser as ReturnType<typeof vi.fn>)({ totp_enabled: false });
updateUser({ totp_enabled: false });
}),
});
const overlay = createSettingsOverlay(options);
@@ -913,7 +913,12 @@ describe("VoiceAudioTab UI structure", () => {
addEventListenerSpy.mock.calls.filter(([type]) => type === "abort").length;
const tab = createVoiceAudioTab(ac.signal);
const afterCreate = abortListenerCount();
// The first build may add jsdom's single internal bookkeeping listener
// for `{ signal }`-scoped DOM listeners; snapshot after it so the
// assertion isolates listener growth caused by rebuilds.
tab.build();
const afterFirstBuild = abortListenerCount();
tab.build();
tab.build();
@@ -922,7 +927,7 @@ describe("VoiceAudioTab UI structure", () => {
// Rebuilding the tab three times must not add three more permanent
// "abort" listeners to the overlay-lifetime signal — only the single
// listener the factory registers once at creation should exist.
expect(abortListenerCount()).toBe(afterCreate);
expect(abortListenerCount()).toBe(afterFirstBuild);
ac.abort();
});
+6 -3
View File
@@ -1,7 +1,10 @@
{
"extends": "./tsconfig.json",
"include": [
"src"
],
// App code is browser-only: reset the "types": ["node"] the dev config
// needs for unit tests, so Node globals stay unavailable in src/.
"compilerOptions": {
"types": []
},
"include": ["src"],
"exclude": []
}
+6
View File
@@ -4,6 +4,12 @@
// the app graph. "exclude": [] is required to clear the inherited
// "tests/e2e" exclusion — same trick tsconfig.build.json uses.
"extends": "./tsconfig.json",
// TypeScript 6 defaults "types" to [] instead of every installed @types
// package, so the Node globals (process, Buffer) used by the Playwright
// layer must be pulled in explicitly.
"compilerOptions": {
"types": ["node"]
},
"include": [
"tests/e2e",
"playwright.config.ts",
+5
View File
@@ -12,6 +12,11 @@
"isolatedModules": true,
"noEmit": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
// TypeScript 6 defaults "types" to [] and vitest 4 no longer references
// @types/node itself, so the Node globals (node:fs, __dirname) used by
// unit tests must be pulled in explicitly. tsconfig.build.json resets
// this to [] so app code cannot use Node globals.
"types": ["node"],
"paths": {
"@lib/*": ["./src/lib/*"],
"@stores/*": ["./src/stores/*"],
+10 -8
View File
@@ -20,20 +20,22 @@ export default defineConfig({
cssCodeSplit: false,
rollupOptions: {
output: {
manualChunks: {
// Keep the ~1.3 MB LiveKit SDK in its own chunk, out of the entry.
livekit: ["livekit-client"],
// Keep the ~1.3 MB LiveKit SDK in its own chunk, out of the entry.
// Rolldown (Vite 8) only supports the function form of manualChunks.
manualChunks(id) {
if (id.includes("node_modules/livekit-client/")) return "livekit";
return undefined;
},
},
},
},
resolve: {
alias: {
"@lib": resolve(__dirname, "src/lib"),
"@stores": resolve(__dirname, "src/stores"),
"@components": resolve(__dirname, "src/components"),
"@pages": resolve(__dirname, "src/pages"),
"@styles": resolve(__dirname, "src/styles"),
"@lib": resolve(import.meta.dirname, "src/lib"),
"@stores": resolve(import.meta.dirname, "src/stores"),
"@components": resolve(import.meta.dirname, "src/components"),
"@pages": resolve(import.meta.dirname, "src/pages"),
"@styles": resolve(import.meta.dirname, "src/styles"),
},
},
clearScreen: false,
+7 -6
View File
@@ -1,20 +1,21 @@
import { defineConfig } from "vitest/config";
import { playwright } from "@vitest/browser-playwright";
import { resolve } from "path";
export default defineConfig({
resolve: {
alias: {
"@lib": resolve(__dirname, "src/lib"),
"@stores": resolve(__dirname, "src/stores"),
"@components": resolve(__dirname, "src/components"),
"@pages": resolve(__dirname, "src/pages"),
"@styles": resolve(__dirname, "src/styles"),
"@lib": resolve(import.meta.dirname, "src/lib"),
"@stores": resolve(import.meta.dirname, "src/stores"),
"@components": resolve(import.meta.dirname, "src/components"),
"@pages": resolve(import.meta.dirname, "src/pages"),
"@styles": resolve(import.meta.dirname, "src/styles"),
},
},
test: {
browser: {
enabled: true,
provider: "playwright",
provider: playwright(),
instances: [{ browser: "chromium" }],
},
include: ["tests/browser/**/*.test.ts"],
+5 -5
View File
@@ -4,11 +4,11 @@ import { resolve } from "path";
export default defineConfig({
resolve: {
alias: {
"@lib": resolve(__dirname, "src/lib"),
"@stores": resolve(__dirname, "src/stores"),
"@components": resolve(__dirname, "src/components"),
"@pages": resolve(__dirname, "src/pages"),
"@styles": resolve(__dirname, "src/styles"),
"@lib": resolve(import.meta.dirname, "src/lib"),
"@stores": resolve(import.meta.dirname, "src/stores"),
"@components": resolve(import.meta.dirname, "src/components"),
"@pages": resolve(import.meta.dirname, "src/pages"),
"@styles": resolve(import.meta.dirname, "src/styles"),
},
},
test: {
+3 -3
View File
@@ -2,7 +2,7 @@ module github.com/owncord/server
go 1.26
toolchain go1.26.6
toolchain go1.26.7
require (
aead.dev/minisign v0.3.0
@@ -10,7 +10,7 @@ require (
github.com/coder/websocket v1.8.15
github.com/corazawaf/coraza-coreruleset/v4 v4.25.0
github.com/corazawaf/coraza/v3 v3.7.0
github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/chi/v5 v5.3.2
github.com/google/uuid v1.6.0
github.com/knadh/koanf/parsers/yaml v1.1.1
github.com/knadh/koanf/providers/env v1.1.0
@@ -38,7 +38,7 @@ require (
golang.org/x/sync v0.22.0
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.56.0
modernc.org/sqlite v1.57.0
)
require (
+4 -4
View File
@@ -72,8 +72,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ=
github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -375,8 +375,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=