diff --git a/Client/tauri-client/tests/browser/smoke.test.ts b/Client/tauri-client/tests/browser/smoke.test.ts
new file mode 100644
index 00000000..2e11b548
--- /dev/null
+++ b/Client/tauri-client/tests/browser/smoke.test.ts
@@ -0,0 +1,19 @@
+import { expect, test } from "vitest";
+
+test("browser environment is available", () => {
+ expect(typeof window).toBe("object");
+ expect(typeof document).toBe("object");
+ expect(typeof document.createElement).toBe("function");
+});
+
+test("real DOM APIs work", () => {
+ const div = document.createElement("div");
+ div.innerHTML = "hello";
+ document.body.appendChild(div);
+
+ const span = document.querySelector("span");
+ expect(span).not.toBeNull();
+ expect(span!.textContent).toBe("hello");
+
+ div.remove();
+});
diff --git a/Client/tauri-client/tests/e2e/channel-sidebar.spec.ts b/Client/tauri-client/tests/e2e/channel-sidebar.spec.ts
index 5090cf39..75f01d46 100644
--- a/Client/tauri-client/tests/e2e/channel-sidebar.spec.ts
+++ b/Client/tauri-client/tests/e2e/channel-sidebar.spec.ts
@@ -1,5 +1,9 @@
import { test, expect } from "@playwright/test";
-import { mockTauriFullSession, mockTauriFullSessionWithMessages, navigateToMainPage } from "./helpers";
+import {
+ mockTauriFullSession,
+ mockTauriFullSessionWithMessages,
+ navigateToMainPage,
+} from "./helpers";
// ---------------------------------------------------------------------------
// Tests: Channel Sidebar
diff --git a/Client/tauri-client/tests/e2e/channel-switch-messages.spec.ts b/Client/tauri-client/tests/e2e/channel-switch-messages.spec.ts
index 5744a142..2af4532b 100644
--- a/Client/tauri-client/tests/e2e/channel-switch-messages.spec.ts
+++ b/Client/tauri-client/tests/e2e/channel-switch-messages.spec.ts
@@ -77,7 +77,7 @@ test.describe("Channel Switch — Messages", () => {
// The message should NOT be visible in current view
await expect(
- page.locator(".msg-text", { hasText: "Message on other channel" })
+ page.locator(".msg-text", { hasText: "Message on other channel" }),
).not.toBeVisible();
});
diff --git a/Client/tauri-client/tests/e2e/connect-settings.spec.ts b/Client/tauri-client/tests/e2e/connect-settings.spec.ts
index b42786f7..586a6d0c 100644
--- a/Client/tauri-client/tests/e2e/connect-settings.spec.ts
+++ b/Client/tauri-client/tests/e2e/connect-settings.spec.ts
@@ -7,12 +7,14 @@ import { buildTauriMockScript } from "./helpers";
test.describe("Connect Page — Settings Overlay", () => {
test.beforeEach(async ({ page }) => {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- ],
- simulateWsFlow: false,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ ],
+ simulateWsFlow: false,
+ }),
+ );
await page.goto("/");
});
diff --git a/Client/tauri-client/tests/e2e/dm-system.spec.ts b/Client/tauri-client/tests/e2e/dm-system.spec.ts
index ee5480b6..a1a8e195 100644
--- a/Client/tauri-client/tests/e2e/dm-system.spec.ts
+++ b/Client/tauri-client/tests/e2e/dm-system.spec.ts
@@ -95,9 +95,9 @@ async function navigateToMainPageWithDms(page: import("@playwright/test").Page):
await waitForWsReady(page);
// Wait for the DM section to render in the unified sidebar
// In channels mode: DM section = .sidebar-dm-section, DM entries = [data-testid="dm-entry"]
- await expect(
- page.locator(".sidebar-dm-section, [data-testid='dm-entry']").first(),
- ).toBeVisible({ timeout: 5_000 });
+ await expect(page.locator(".sidebar-dm-section, [data-testid='dm-entry']").first()).toBeVisible({
+ timeout: 5_000,
+ });
}
// ---------------------------------------------------------------------------
@@ -245,9 +245,7 @@ test.describe("DM System — WS Events", () => {
});
// The unread count should increment. In channels mode, DM entries use .dm-unread-badge
- await expect(
- page.locator(".dm-unread-badge").first(),
- ).toBeVisible({ timeout: 5_000 });
+ await expect(page.locator(".dm-unread-badge").first()).toBeVisible({ timeout: 5_000 });
});
});
diff --git a/Client/tauri-client/tests/e2e/health-status.spec.ts b/Client/tauri-client/tests/e2e/health-status.spec.ts
index 4185cfbc..33039b43 100644
--- a/Client/tauri-client/tests/e2e/health-status.spec.ts
+++ b/Client/tauri-client/tests/e2e/health-status.spec.ts
@@ -7,12 +7,14 @@ import { buildTauriMockScript } from "./helpers";
test.describe("Health Status Indicator", () => {
test.beforeEach(async ({ page }) => {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- ],
- simulateWsFlow: false,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ ],
+ simulateWsFlow: false,
+ }),
+ );
await page.goto("/");
});
diff --git a/Client/tauri-client/tests/e2e/helpers.ts b/Client/tauri-client/tests/e2e/helpers.ts
index 278cd367..9da61c42 100644
--- a/Client/tauri-client/tests/e2e/helpers.ts
+++ b/Client/tauri-client/tests/e2e/helpers.ts
@@ -160,7 +160,13 @@ export const MOCK_MESSAGES_RICH = {
timestamp: "2026-03-15T10:03:00Z",
edited_at: null,
attachments: [
- { id: "1", filename: "screenshot.png", size: 102400, mime: "image/png", url: "/uploads/screenshot.png" },
+ {
+ id: "1",
+ filename: "screenshot.png",
+ size: 102400,
+ mime: "image/png",
+ url: "/uploads/screenshot.png",
+ },
],
reactions: [],
reply_to: null,
@@ -175,7 +181,13 @@ export const MOCK_MESSAGES_RICH = {
timestamp: "2026-03-15T10:03:30Z",
edited_at: null,
attachments: [
- { id: "2", filename: "report.pdf", size: 512000, mime: "application/pdf", url: "/uploads/report.pdf" },
+ {
+ id: "2",
+ filename: "report.pdf",
+ size: 512000,
+ mime: "application/pdf",
+ url: "/uploads/report.pdf",
+ },
],
reactions: [],
reply_to: null,
@@ -557,13 +569,19 @@ export function buildTauriMockScript(opts: {
// ---- WS commands ----
if (cmd === "ws_connect") {
- ${opts.simulateWsFlow ? `
+ ${
+ opts.simulateWsFlow
+ ? `
setTimeout(() => __tauriEmitEvent("ws-state", "open"), 100);
- ` : ""}
+ `
+ : ""
+ }
return;
}
if (cmd === "ws_send") {
- ${opts.simulateWsFlow ? `
+ ${
+ opts.simulateWsFlow
+ ? `
try {
var parsed = JSON.parse(args?.message || "{}");
if (parsed.type === "auth") {
@@ -582,7 +600,9 @@ export function buildTauriMockScript(opts: {
}
}
} catch (e) {}
- ` : ""}
+ `
+ : ""
+ }
return;
}
if (cmd === "ws_disconnect") return;
@@ -618,136 +638,164 @@ export function buildTauriMockScript(opts: {
// ---------------------------------------------------------------------------
export async function mockTauriConnect(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- ],
- simulateWsFlow: false,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ ],
+ simulateWsFlow: false,
+ }),
+ );
}
export async function mockTauriConnectWith2FA(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_2FA_RESPONSE },
- ],
- simulateWsFlow: false,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_2FA_RESPONSE },
+ ],
+ simulateWsFlow: false,
+ }),
+ );
}
export async function mockTauriFullSession(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
- { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
- { pattern: "/pins", status: 200, body: MOCK_PINNED_MESSAGES },
- ],
- simulateWsFlow: true,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
+ { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
+ { pattern: "/pins", status: 200, body: MOCK_PINNED_MESSAGES },
+ ],
+ simulateWsFlow: true,
+ }),
+ );
}
export async function mockTauriFullSessionWithMessages(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
- { pattern: "/messages", status: 200, body: MOCK_MESSAGES_RICH },
- { pattern: "/pins", status: 200, body: MOCK_PINNED_MESSAGES },
- { pattern: "/api/v1/invites", status: 200, body: MOCK_INVITES },
- ],
- simulateWsFlow: true,
- readyOverrides: {
- channels: MOCK_CHANNELS_WITH_CATEGORIES,
- members: MOCK_MEMBERS_MULTI_ROLE,
- },
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
+ { pattern: "/messages", status: 200, body: MOCK_MESSAGES_RICH },
+ { pattern: "/pins", status: 200, body: MOCK_PINNED_MESSAGES },
+ { pattern: "/api/v1/invites", status: 200, body: MOCK_INVITES },
+ ],
+ simulateWsFlow: true,
+ readyOverrides: {
+ channels: MOCK_CHANNELS_WITH_CATEGORIES,
+ members: MOCK_MEMBERS_MULTI_ROLE,
+ },
+ }),
+ );
}
export async function mockTauriFullSessionWithVoice(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
- { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
- ],
- simulateWsFlow: true,
- wsHandlers: voiceWsHandlers(),
- readyOverrides: {
- channels: MOCK_CHANNELS_WITH_CATEGORIES,
- members: MOCK_MEMBERS_MULTI_ROLE,
- voice_states: MOCK_VOICE_STATE,
- },
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
+ { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
+ ],
+ simulateWsFlow: true,
+ wsHandlers: voiceWsHandlers(),
+ readyOverrides: {
+ channels: MOCK_CHANNELS_WITH_CATEGORIES,
+ members: MOCK_MEMBERS_MULTI_ROLE,
+ voice_states: MOCK_VOICE_STATE,
+ },
+ }),
+ );
}
export async function mockTauriFullSessionWithVoiceFailure(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
- { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
- ],
- simulateWsFlow: true,
- wsHandlers: [voiceJoinFailureHandler()],
- readyOverrides: {
- channels: MOCK_CHANNELS_WITH_CATEGORIES,
- members: MOCK_MEMBERS_MULTI_ROLE,
- voice_states: MOCK_VOICE_STATE,
- },
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
+ { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
+ ],
+ simulateWsFlow: true,
+ wsHandlers: [voiceJoinFailureHandler()],
+ readyOverrides: {
+ channels: MOCK_CHANNELS_WITH_CATEGORIES,
+ members: MOCK_MEMBERS_MULTI_ROLE,
+ voice_states: MOCK_VOICE_STATE,
+ },
+ }),
+ );
}
export async function mockTauriFullSessionWithEcho(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
- { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
- ],
- simulateWsFlow: true,
- echoChatSend: true,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
+ { pattern: "/messages", status: 200, body: MOCK_MESSAGES },
+ ],
+ simulateWsFlow: true,
+ echoChatSend: true,
+ }),
+ );
}
export async function mockTauriFullSessionWithMessagesAndEcho(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
- { pattern: "/messages", status: 200, body: MOCK_MESSAGES_RICH },
- { pattern: "/pins", status: 200, body: MOCK_PINNED_MESSAGES },
- { pattern: "/api/v1/invites", status: 200, body: MOCK_INVITES },
- ],
- simulateWsFlow: true,
- echoChatSend: true,
- readyOverrides: {
- channels: MOCK_CHANNELS_WITH_CATEGORIES,
- members: MOCK_MEMBERS_MULTI_ROLE,
- },
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
+ { pattern: "/messages", status: 200, body: MOCK_MESSAGES_RICH },
+ { pattern: "/pins", status: 200, body: MOCK_PINNED_MESSAGES },
+ { pattern: "/api/v1/invites", status: 200, body: MOCK_INVITES },
+ ],
+ simulateWsFlow: true,
+ echoChatSend: true,
+ readyOverrides: {
+ channels: MOCK_CHANNELS_WITH_CATEGORIES,
+ members: MOCK_MEMBERS_MULTI_ROLE,
+ },
+ }),
+ );
}
export async function mockTauriFullSessionWithFailingMessages(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
- { pattern: "/messages", status: 500, body: { error: "INTERNAL_ERROR", message: "Failed to load messages" } },
- ],
- simulateWsFlow: true,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_RESPONSE },
+ {
+ pattern: "/messages",
+ status: 500,
+ body: { error: "INTERNAL_ERROR", message: "Failed to load messages" },
+ },
+ ],
+ simulateWsFlow: true,
+ }),
+ );
}
export async function mockTauriLoginError(page: Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 401, body: { error: "INVALID_CREDENTIALS", message: "Invalid username or password" } },
- ],
- simulateWsFlow: false,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ {
+ pattern: "/api/v1/auth/login",
+ status: 401,
+ body: { error: "INVALID_CREDENTIALS", message: "Invalid username or password" },
+ },
+ ],
+ simulateWsFlow: false,
+ }),
+ );
}
// ---------------------------------------------------------------------------
@@ -794,15 +842,14 @@ export async function switchSettingsTab(page: Page, tabName: string): Promise {
+export async function emitWsEvent(page: Page, eventName: string, payload: unknown): Promise {
await page.evaluate(
({ event, data }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- (window as any).__tauriEmitEvent(event, typeof data === "string" ? data : JSON.stringify(data));
+ (window as any).__tauriEmitEvent(
+ event,
+ typeof data === "string" ? data : JSON.stringify(data),
+ );
},
{ event: eventName, data: payload },
);
diff --git a/Client/tauri-client/tests/e2e/logout-flow.spec.ts b/Client/tauri-client/tests/e2e/logout-flow.spec.ts
index 1ea5fac4..cdc3b4c4 100644
--- a/Client/tauri-client/tests/e2e/logout-flow.spec.ts
+++ b/Client/tauri-client/tests/e2e/logout-flow.spec.ts
@@ -12,15 +12,11 @@ test.describe("Logout Flow", () => {
await navigateToMainPage(page);
});
- test("clicking Log Out in settings returns to connect page", async ({
- page,
- }) => {
+ test("clicking Log Out in settings returns to connect page", async ({ page }) => {
// Open settings
const settingsBtn = page.locator("button[aria-label='Settings']");
await settingsBtn.click();
- await expect(
- page.locator(".settings-overlay.open"),
- ).toBeVisible({ timeout: 3000 });
+ await expect(page.locator(".settings-overlay.open")).toBeVisible({ timeout: 3000 });
// Click Log Out button
const logoutBtn = page.locator(".settings-nav-item.danger", {
@@ -37,9 +33,7 @@ test.describe("Logout Flow", () => {
// Open settings and log out
const settingsBtn = page.locator("button[aria-label='Settings']");
await settingsBtn.click();
- await expect(
- page.locator(".settings-overlay.open"),
- ).toBeVisible({ timeout: 3000 });
+ await expect(page.locator(".settings-overlay.open")).toBeVisible({ timeout: 3000 });
const logoutBtn = page.locator(".settings-nav-item.danger", {
hasText: "Log Out",
diff --git a/Client/tauri-client/tests/e2e/message-actions.spec.ts b/Client/tauri-client/tests/e2e/message-actions.spec.ts
index 38a54260..fe8995e6 100644
--- a/Client/tauri-client/tests/e2e/message-actions.spec.ts
+++ b/Client/tauri-client/tests/e2e/message-actions.spec.ts
@@ -3,10 +3,7 @@
* Tests: reply, edit, delete buttons on message hover.
*/
import { test, expect } from "@playwright/test";
-import {
- mockTauriFullSessionWithMessagesAndEcho,
- navigateToMainPage,
-} from "./helpers";
+import { mockTauriFullSessionWithMessagesAndEcho, navigateToMainPage } from "./helpers";
test.describe("Message Actions Bar", () => {
test.beforeEach(async ({ page }) => {
@@ -69,9 +66,7 @@ test.describe("Message Actions Bar", () => {
await expect(replyBar).toBeVisible({ timeout: 3000 });
});
- test("clicking Edit populates textarea with message content", async ({
- page,
- }) => {
+ test("clicking Edit populates textarea with message content", async ({ page }) => {
const ownMessage = page.locator("[data-testid='message-101']");
await ownMessage.hover();
@@ -99,9 +94,7 @@ test.describe("Message Reactions", () => {
await navigateToMainPage(page);
});
- test("reaction chips are visible on messages with reactions", async ({
- page,
- }) => {
+ test("reaction chips are visible on messages with reactions", async ({ page }) => {
const reactions = page.locator(".msg-reactions");
await expect(reactions.first()).toBeVisible();
});
diff --git a/Client/tauri-client/tests/e2e/message-edit-delete.spec.ts b/Client/tauri-client/tests/e2e/message-edit-delete.spec.ts
index d5403764..f32a6868 100644
--- a/Client/tauri-client/tests/e2e/message-edit-delete.spec.ts
+++ b/Client/tauri-client/tests/e2e/message-edit-delete.spec.ts
@@ -3,10 +3,7 @@
* Covers: edit → save, edit → cancel, delete.
*/
import { test, expect } from "@playwright/test";
-import {
- mockTauriFullSessionWithMessagesAndEcho,
- navigateToMainPage,
-} from "./helpers";
+import { mockTauriFullSessionWithMessagesAndEcho, navigateToMainPage } from "./helpers";
test.describe("Message Edit Flow", () => {
test.beforeEach(async ({ page }) => {
@@ -97,8 +94,8 @@ test.describe("Message Delete Flow", () => {
await deleteBtn.click();
// Soft-delete: message stays in DOM but shows "[message deleted]"
- await expect(
- ownMessage.locator(".msg-text", { hasText: "[message deleted]" }),
- ).toBeVisible({ timeout: 5000 });
+ await expect(ownMessage.locator(".msg-text", { hasText: "[message deleted]" })).toBeVisible({
+ timeout: 5000,
+ });
});
});
diff --git a/Client/tauri-client/tests/e2e/message-list.spec.ts b/Client/tauri-client/tests/e2e/message-list.spec.ts
index 8d91e557..34964591 100644
--- a/Client/tauri-client/tests/e2e/message-list.spec.ts
+++ b/Client/tauri-client/tests/e2e/message-list.spec.ts
@@ -127,9 +127,9 @@ test.describe("Message List — Real-time", () => {
}
for (let i = 0; i < 3; i++) {
- await expect(
- page.locator(".msg-text", { hasText: `Rapid message ${i}` })
- ).toBeVisible({ timeout: 5_000 });
+ await expect(page.locator(".msg-text", { hasText: `Rapid message ${i}` })).toBeVisible({
+ timeout: 5_000,
+ });
}
});
});
diff --git a/Client/tauri-client/tests/e2e/message-send-flow.spec.ts b/Client/tauri-client/tests/e2e/message-send-flow.spec.ts
index 2daab154..a1988aa6 100644
--- a/Client/tauri-client/tests/e2e/message-send-flow.spec.ts
+++ b/Client/tauri-client/tests/e2e/message-send-flow.spec.ts
@@ -3,10 +3,7 @@
* Covers: type message → send → see it appear in message list.
*/
import { test, expect } from "@playwright/test";
-import {
- mockTauriFullSessionWithEcho,
- navigateToMainPage,
-} from "./helpers";
+import { mockTauriFullSessionWithEcho, navigateToMainPage } from "./helpers";
test.describe("Message Send Flow", () => {
test.beforeEach(async ({ page }) => {
@@ -46,9 +43,9 @@ test.describe("Message Send Flow", () => {
await textarea.press("Enter");
// Wait for the echo message to appear (confirms send happened)
- await expect(
- page.locator(".message .msg-text", { hasText: "Clear after send" }),
- ).toBeVisible({ timeout: 5000 });
+ await expect(page.locator(".message .msg-text", { hasText: "Clear after send" })).toBeVisible({
+ timeout: 5000,
+ });
// Textarea should be empty
await expect(textarea).toHaveValue("");
diff --git a/Client/tauri-client/tests/e2e/native-fixture-persistent.ts b/Client/tauri-client/tests/e2e/native-fixture-persistent.ts
index 5a1fff5a..a01ffbeb 100644
--- a/Client/tauri-client/tests/e2e/native-fixture-persistent.ts
+++ b/Client/tauri-client/tests/e2e/native-fixture-persistent.ts
@@ -28,10 +28,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/** Path to the built Tauri exe (release build). */
-const TAURI_EXE = path.resolve(
- __dirname,
- "../../src-tauri/target/release/owncord-client.exe",
-);
+const TAURI_EXE = path.resolve(__dirname, "../../src-tauri/target/release/owncord-client.exe");
/** CDP port for WebView2 remote debugging. */
const CDP_PORT = parseInt(process.env.CDP_PORT ?? "9222", 10);
@@ -65,7 +62,7 @@ async function waitForCdpEndpoint(port: number, timeout: number): Promise
throw new Error(
`CDP endpoint at port ${port} did not become available within ${timeout}ms. ` +
- `Make sure the Tauri app was built (npm run tauri build) and the exe exists at: ${TAURI_EXE}`,
+ `Make sure the Tauri app was built (npm run tauri build) and the exe exists at: ${TAURI_EXE}`,
);
}
@@ -119,7 +116,7 @@ async function acquirePersistentPage(workerIndex: number): Promise
throw new Error(
`CDP endpoint at port ${port} did not become available within ${timeout}ms. ` +
- `Make sure the Tauri app was built (npm run tauri build) and the exe exists at: ${TAURI_EXE}`,
+ `Make sure the Tauri app was built (npm run tauri build) and the exe exists at: ${TAURI_EXE}`,
);
}
@@ -118,7 +115,7 @@ export const test = base.extend({
if (!fs.existsSync(TAURI_EXE)) {
throw new Error(
`Tauri exe not found at: ${TAURI_EXE}\n` +
- `Run 'npm run tauri build' first to create the production build.`,
+ `Run 'npm run tauri build' first to create the production build.`,
);
}
diff --git a/Client/tauri-client/tests/e2e/native/dm-system.spec.ts b/Client/tauri-client/tests/e2e/native/dm-system.spec.ts
index ac3cf0e4..e67680c8 100644
--- a/Client/tauri-client/tests/e2e/native/dm-system.spec.ts
+++ b/Client/tauri-client/tests/e2e/native/dm-system.spec.ts
@@ -6,12 +6,7 @@
*/
import { test, expect } from "../native-fixture-persistent";
-import {
- SKIP_SERVER,
- hasCredentials,
- ensureLoggedIn,
- waitForMessages,
-} from "./helpers";
+import { SKIP_SERVER, hasCredentials, ensureLoggedIn, waitForMessages } from "./helpers";
test.describe.configure({ mode: "serial" });
diff --git a/Client/tauri-client/tests/e2e/native/overlays.spec.ts b/Client/tauri-client/tests/e2e/native/overlays.spec.ts
index 3c0c891c..ab4961b5 100644
--- a/Client/tauri-client/tests/e2e/native/overlays.spec.ts
+++ b/Client/tauri-client/tests/e2e/native/overlays.spec.ts
@@ -148,7 +148,9 @@ test.describe("Pinned Messages", () => {
test("pin button triggers pin action", async ({ nativePage }) => {
// The pin button may be a standalone icon, not a data-testid element.
// From production screenshots: it's the 📌 icon in the chat header.
- const pinBtn = nativePage.locator("[data-testid='pin-btn'], .pin-btn, button[aria-label='Pins']").first();
+ const pinBtn = nativePage
+ .locator("[data-testid='pin-btn'], .pin-btn, button[aria-label='Pins']")
+ .first();
const exists = await pinBtn.isVisible().catch(() => false);
test.skip(!exists, "No pin button in chat header");
@@ -170,14 +172,19 @@ test.describe("Pinned Messages", () => {
});
test("pinned panel can be closed when available", async ({ nativePage }) => {
- const pinBtn = nativePage.locator("[data-testid='pin-btn'], .pin-btn, button[aria-label='Pins']").first();
+ const pinBtn = nativePage
+ .locator("[data-testid='pin-btn'], .pin-btn, button[aria-label='Pins']")
+ .first();
const exists = await pinBtn.isVisible().catch(() => false);
test.skip(!exists, "No pin button in chat header");
await pinBtn.click();
const panel = nativePage.locator(".pinned-panel");
- const panelVisible = await panel.waitFor({ state: "visible", timeout: 5_000 }).then(() => true).catch(() => false);
+ const panelVisible = await panel
+ .waitFor({ state: "visible", timeout: 5_000 })
+ .then(() => true)
+ .catch(() => false);
test.skip(!panelVisible, "Pinned panel did not open (server may not have pin data)");
// Close via close button
diff --git a/Client/tauri-client/tests/e2e/native/reconnection.spec.ts b/Client/tauri-client/tests/e2e/native/reconnection.spec.ts
index 102a78e1..af680f39 100644
--- a/Client/tauri-client/tests/e2e/native/reconnection.spec.ts
+++ b/Client/tauri-client/tests/e2e/native/reconnection.spec.ts
@@ -7,11 +7,7 @@
*/
import { test, expect } from "../native-fixture-persistent";
-import {
- SKIP_SERVER,
- hasCredentials,
- ensureLoggedIn,
-} from "./helpers";
+import { SKIP_SERVER, hasCredentials, ensureLoggedIn } from "./helpers";
// ---------------------------------------------------------------------------
// Tests: Connection State
@@ -29,11 +25,9 @@ test.describe("Reconnection (Native)", () => {
test("reconnecting banner is NOT visible when connected", async ({ nativePage }) => {
const banner = nativePage.locator(".reconnecting-banner");
- if (await banner.count() > 0) {
+ if ((await banner.count()) > 0) {
// Banner element may exist in the DOM but should not be visible
- const isVisible = await banner.evaluate((el) =>
- el.classList.contains("visible"),
- );
+ const isVisible = await banner.evaluate((el) => el.classList.contains("visible"));
expect(isVisible).toBe(false);
}
// If the banner element doesn't exist at all, that's also fine
@@ -91,7 +85,9 @@ test.describe("Reconnection (Native)", () => {
}
// 2. Message input is usable
- const input = nativePage.locator("[data-testid='message-input'], .message-input-field, textarea.msg-box");
+ const input = nativePage.locator(
+ "[data-testid='message-input'], .message-input-field, textarea.msg-box",
+ );
if (await input.isVisible().catch(() => false)) {
await input.focus();
// Input should accept focus without errors
@@ -99,10 +95,8 @@ test.describe("Reconnection (Native)", () => {
// 3. No error banners visible
const banner = nativePage.locator(".reconnecting-banner");
- if (await banner.count() > 0) {
- const bannerVisible = await banner.evaluate((el) =>
- el.classList.contains("visible"),
- );
+ if ((await banner.count()) > 0) {
+ const bannerVisible = await banner.evaluate((el) => el.classList.contains("visible"));
expect(bannerVisible).toBe(false);
}
});
diff --git a/Client/tauri-client/tests/e2e/native/smoke.spec.ts b/Client/tauri-client/tests/e2e/native/smoke.spec.ts
index fb974967..19ce30bb 100644
--- a/Client/tauri-client/tests/e2e/native/smoke.spec.ts
+++ b/Client/tauri-client/tests/e2e/native/smoke.spec.ts
@@ -95,10 +95,7 @@ test.describe("Native App Server Connection", () => {
test("health check via real Tauri HTTP plugin", async ({ nativePage }) => {
// This test requires chatserver.exe to be running.
// Skip if OWNCORD_SKIP_SERVER_TESTS is set.
- test.skip(
- !!process.env.OWNCORD_SKIP_SERVER_TESTS,
- "Skipped: OWNCORD_SKIP_SERVER_TESTS is set",
- );
+ test.skip(!!process.env.OWNCORD_SKIP_SERVER_TESTS, "Skipped: OWNCORD_SKIP_SERVER_TESTS is set");
await nativePage.waitForLoadState("networkidle");
@@ -132,10 +129,7 @@ test.describe("Native App Server Connection", () => {
// It does NOT require valid credentials — an "invalid credentials" error
// from the server proves the round-trip works.
// Skip if OWNCORD_SKIP_SERVER_TESTS is set.
- test.skip(
- !!process.env.OWNCORD_SKIP_SERVER_TESTS,
- "Skipped: OWNCORD_SKIP_SERVER_TESTS is set",
- );
+ test.skip(!!process.env.OWNCORD_SKIP_SERVER_TESTS, "Skipped: OWNCORD_SKIP_SERVER_TESTS is set");
await nativePage.waitForLoadState("networkidle");
@@ -152,14 +146,14 @@ test.describe("Native App Server Connection", () => {
// Wait for either: successful login OR server error response.
// Both prove the real HTTP plugin made a round-trip to the server.
const appLayout = nativePage.locator("[data-testid='app-layout']");
- const errorBanner = nativePage.locator(".error-banner, .error-message, .toast-error, [role='alert']");
+ const errorBanner = nativePage.locator(
+ ".error-banner, .error-message, .toast-error, [role='alert']",
+ );
// Use Promise.race — whichever appears first
const result = await Promise.race([
- appLayout.waitFor({ state: "visible", timeout: 20_000 })
- .then(() => "login-success" as const),
- errorBanner.waitFor({ state: "visible", timeout: 20_000 })
- .then(() => "login-error" as const),
+ appLayout.waitFor({ state: "visible", timeout: 20_000 }).then(() => "login-success" as const),
+ errorBanner.waitFor({ state: "visible", timeout: 20_000 }).then(() => "login-error" as const),
]).catch(() => "timeout" as const);
// Either outcome proves the real Tauri HTTP plugin works
@@ -173,10 +167,9 @@ test.describe("Native App Credential Store", () => {
// (save_credential, load_credential, delete_credential)
const canInvoke = await nativePage.evaluate(async () => {
try {
- const result = await (window as any).__TAURI_INTERNALS__.invoke(
- "load_credential",
- { host: "e2e-test-nonexistent" },
- );
+ const result = await (window as any).__TAURI_INTERNALS__.invoke("load_credential", {
+ host: "e2e-test-nonexistent",
+ });
// Should return null for nonexistent host, not throw
return result === null || result === undefined;
} catch (e: any) {
diff --git a/Client/tauri-client/tests/e2e/native/theme-persistence.spec.ts b/Client/tauri-client/tests/e2e/native/theme-persistence.spec.ts
index dd01449f..eef95c5a 100644
--- a/Client/tauri-client/tests/e2e/native/theme-persistence.spec.ts
+++ b/Client/tauri-client/tests/e2e/native/theme-persistence.spec.ts
@@ -6,12 +6,7 @@
*/
import { test, expect } from "../native-fixture-persistent";
-import {
- SKIP_SERVER,
- hasCredentials,
- ensureLoggedIn,
- openSettings,
-} from "./helpers";
+import { SKIP_SERVER, hasCredentials, ensureLoggedIn, openSettings } from "./helpers";
// ---------------------------------------------------------------------------
// Helper: switch to a settings tab by name
@@ -56,9 +51,7 @@ test.describe("Theme Persistence (Native)", () => {
// Find and click an inactive theme
for (let i = 0; i < count; i++) {
- const isActive = await themeOptions.nth(i).evaluate((el) =>
- el.classList.contains("active"),
- );
+ const isActive = await themeOptions.nth(i).evaluate((el) => el.classList.contains("active"));
if (!isActive) {
await themeOptions.nth(i).click();
break;
@@ -76,7 +69,9 @@ test.describe("Theme Persistence (Native)", () => {
test("accent color picker applies CSS variable", async ({ nativePage }) => {
// Look for accent color input
- const colorInput = nativePage.locator("input[type='color'], .accent-color-input, .accent-picker");
+ const colorInput = nativePage.locator(
+ "input[type='color'], .accent-color-input, .accent-picker",
+ );
if (await colorInput.isVisible().catch(() => false)) {
await colorInput.fill("#ff0066");
@@ -126,7 +121,9 @@ test.describe("Theme Persistence (Native)", () => {
});
test("compact mode toggle adds class to body", async ({ nativePage }) => {
- const toggle = nativePage.locator(".setting-row", { hasText: "Compact Mode" }).locator(".toggle");
+ const toggle = nativePage
+ .locator(".setting-row", { hasText: "Compact Mode" })
+ .locator(".toggle");
await expect(toggle).toBeVisible();
const wasCompact = await nativePage.evaluate(() =>
diff --git a/Client/tauri-client/tests/e2e/overlays.spec.ts b/Client/tauri-client/tests/e2e/overlays.spec.ts
index 326340f3..1e551d85 100644
--- a/Client/tauri-client/tests/e2e/overlays.spec.ts
+++ b/Client/tauri-client/tests/e2e/overlays.spec.ts
@@ -1,5 +1,9 @@
import { test, expect } from "@playwright/test";
-import { mockTauriFullSession, mockTauriFullSessionWithMessages, navigateToMainPage } from "./helpers";
+import {
+ mockTauriFullSession,
+ mockTauriFullSessionWithMessages,
+ navigateToMainPage,
+} from "./helpers";
// ---------------------------------------------------------------------------
// Tests: Quick Switcher (Ctrl+K)
@@ -67,10 +71,9 @@ test.describe("Quick Switcher", () => {
const initialCount = await page.locator(".quick-switcher__item").count();
await input.fill("general");
- await expect.poll(
- async () => page.locator(".quick-switcher__item").count(),
- { timeout: 2000 },
- ).toBeGreaterThan(0);
+ await expect
+ .poll(async () => page.locator(".quick-switcher__item").count(), { timeout: 2000 })
+ .toBeGreaterThan(0);
const filteredCount = await page.locator(".quick-switcher__item").count();
expect(filteredCount).toBeLessThanOrEqual(initialCount);
@@ -162,10 +165,9 @@ test.describe("Emoji Picker", () => {
// Search for a specific emoji character that exists in the grid
await search.fill("\uD83D\uDE00");
- await expect.poll(
- async () => page.locator(".ep-emoji").count(),
- { timeout: 2000 },
- ).toBeGreaterThan(0);
+ await expect
+ .poll(async () => page.locator(".ep-emoji").count(), { timeout: 2000 })
+ .toBeGreaterThan(0);
const countAfter = await allEmojis.count();
// After filtering, should have fewer results
diff --git a/Client/tauri-client/tests/e2e/reconnection.spec.ts b/Client/tauri-client/tests/e2e/reconnection.spec.ts
index 2168e481..ab04c384 100644
--- a/Client/tauri-client/tests/e2e/reconnection.spec.ts
+++ b/Client/tauri-client/tests/e2e/reconnection.spec.ts
@@ -65,7 +65,7 @@ test.describe("Reconnection — Banner Visibility", () => {
test("reconnecting banner is hidden when connected", async ({ page }) => {
const banner = page.locator(".reconnecting-banner");
// The banner element exists but should NOT have the "visible" class
- if (await banner.count() > 0) {
+ if ((await banner.count()) > 0) {
await expect(banner).not.toHaveClass(/visible/);
}
});
@@ -83,7 +83,7 @@ test.describe("Reconnection — Banner Visibility", () => {
await simulateReconnect(page);
const banner = page.locator(".reconnecting-banner");
- if (await banner.count() > 0) {
+ if ((await banner.count()) > 0) {
// After successful reconnect, banner should be hidden
await expect(banner).not.toHaveClass(/visible/);
}
diff --git a/Client/tauri-client/tests/e2e/register-flow.spec.ts b/Client/tauri-client/tests/e2e/register-flow.spec.ts
index 793a53dc..232ae5dd 100644
--- a/Client/tauri-client/tests/e2e/register-flow.spec.ts
+++ b/Client/tauri-client/tests/e2e/register-flow.spec.ts
@@ -11,23 +11,31 @@ const MOCK_REGISTER_RESPONSE = {
};
async function mockRegisterSuccess(page: import("@playwright/test").Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/register", status: 200, body: MOCK_REGISTER_RESPONSE },
- ],
- simulateWsFlow: true,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/register", status: 200, body: MOCK_REGISTER_RESPONSE },
+ ],
+ simulateWsFlow: true,
+ }),
+ );
}
async function mockRegisterConflict(page: import("@playwright/test").Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/register", status: 409, body: { error: "USERNAME_TAKEN", message: "Username already exists" } },
- ],
- simulateWsFlow: false,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ {
+ pattern: "/api/v1/auth/register",
+ status: 409,
+ body: { error: "USERNAME_TAKEN", message: "Username already exists" },
+ },
+ ],
+ simulateWsFlow: false,
+ }),
+ );
}
async function switchToRegisterMode(page: import("@playwright/test").Page): Promise {
diff --git a/Client/tauri-client/tests/e2e/reply-flow.spec.ts b/Client/tauri-client/tests/e2e/reply-flow.spec.ts
index 31c016de..8a15fa45 100644
--- a/Client/tauri-client/tests/e2e/reply-flow.spec.ts
+++ b/Client/tauri-client/tests/e2e/reply-flow.spec.ts
@@ -3,10 +3,7 @@
* Covers: click reply → see reply bar → send reply → verify.
*/
import { test, expect } from "@playwright/test";
-import {
- mockTauriFullSessionWithMessagesAndEcho,
- navigateToMainPage,
-} from "./helpers";
+import { mockTauriFullSessionWithMessagesAndEcho, navigateToMainPage } from "./helpers";
test.describe("Reply Flow", () => {
test.beforeEach(async ({ page }) => {
diff --git a/Client/tauri-client/tests/e2e/settings-overlay.spec.ts b/Client/tauri-client/tests/e2e/settings-overlay.spec.ts
index 05d3fcf5..79058050 100644
--- a/Client/tauri-client/tests/e2e/settings-overlay.spec.ts
+++ b/Client/tauri-client/tests/e2e/settings-overlay.spec.ts
@@ -1,5 +1,10 @@
import { test, expect } from "@playwright/test";
-import { mockTauriFullSession, navigateToMainPage, openSettings, switchSettingsTab } from "./helpers";
+import {
+ mockTauriFullSession,
+ navigateToMainPage,
+ openSettings,
+ switchSettingsTab,
+} from "./helpers";
// ---------------------------------------------------------------------------
// Tests: Settings Overlay — structure
diff --git a/Client/tauri-client/tests/e2e/theme-persistence.spec.ts b/Client/tauri-client/tests/e2e/theme-persistence.spec.ts
index a9617366..b2f6e381 100644
--- a/Client/tauri-client/tests/e2e/theme-persistence.spec.ts
+++ b/Client/tauri-client/tests/e2e/theme-persistence.spec.ts
@@ -42,9 +42,7 @@ test.describe("Theme Persistence", () => {
// Find a theme option that is NOT currently active
let targetIndex = -1;
for (let i = 0; i < count; i++) {
- const isActive = await themeOptions.nth(i).evaluate((el) =>
- el.classList.contains("active"),
- );
+ const isActive = await themeOptions.nth(i).evaluate((el) => el.classList.contains("active"));
if (!isActive) {
targetIndex = i;
break;
@@ -72,9 +70,7 @@ test.describe("Theme Persistence", () => {
await themeOptions.nth(1).click();
// Check localStorage for theme persistence
- const storedTheme = await page.evaluate(() =>
- localStorage.getItem("owncord:theme:active"),
- );
+ const storedTheme = await page.evaluate(() => localStorage.getItem("owncord:theme:active"));
expect(storedTheme).not.toBeNull();
expect(storedTheme!.length).toBeGreaterThan(0);
});
@@ -85,9 +81,7 @@ test.describe("Theme Persistence", () => {
await themeOptions.first().click();
// Read what was stored
- const storedTheme = await page.evaluate(() =>
- localStorage.getItem("owncord:theme:active"),
- );
+ const storedTheme = await page.evaluate(() => localStorage.getItem("owncord:theme:active"));
// Verify the body has the corresponding class
if (storedTheme !== null) {
@@ -144,16 +138,12 @@ test.describe("Accent Color Override", () => {
// Wait for the value to be stored
await expect(async () => {
- const val = await page.evaluate(() =>
- localStorage.getItem("owncord:pref:accentColor"),
- );
+ const val = await page.evaluate(() => localStorage.getItem("owncord:pref:accentColor"));
expect(val).not.toBeNull();
}).toPass({ timeout: 3_000 });
// Read the stored value
- const stored = await page.evaluate(() =>
- localStorage.getItem("owncord:pref:accentColor"),
- );
+ const stored = await page.evaluate(() => localStorage.getItem("owncord:pref:accentColor"));
// Navigate away from Appearance tab and back
await switchSettingsTab(page, "Account");
diff --git a/Client/tauri-client/tests/e2e/totp-flow.spec.ts b/Client/tauri-client/tests/e2e/totp-flow.spec.ts
index bd102fb6..24cd867b 100644
--- a/Client/tauri-client/tests/e2e/totp-flow.spec.ts
+++ b/Client/tauri-client/tests/e2e/totp-flow.spec.ts
@@ -3,31 +3,39 @@
* Covers: valid code submits, invalid code shows error, cancel returns to login.
*/
import { test, expect } from "@playwright/test";
-import {
- buildTauriMockScript,
- MOCK_LOGIN_2FA_RESPONSE,
- MOCK_TOKEN,
-} from "./helpers";
+import { buildTauriMockScript, MOCK_LOGIN_2FA_RESPONSE, MOCK_TOKEN } from "./helpers";
async function mockTotpSuccess(page: import("@playwright/test").Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_2FA_RESPONSE },
- { pattern: "/api/v1/auth/verify-totp", status: 200, body: { token: MOCK_TOKEN, requires_2fa: false } },
- ],
- simulateWsFlow: true,
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_2FA_RESPONSE },
+ {
+ pattern: "/api/v1/auth/verify-totp",
+ status: 200,
+ body: { token: MOCK_TOKEN, requires_2fa: false },
+ },
+ ],
+ simulateWsFlow: true,
+ }),
+ );
}
async function mockTotpFailure(page: import("@playwright/test").Page): Promise {
- await page.addInitScript(buildTauriMockScript({
- httpRoutes: [
- { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
- { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_2FA_RESPONSE },
- { pattern: "/api/v1/auth/verify-totp", status: 401, body: { error: "INVALID_CODE", message: "Invalid verification code" } },
- ],
- }));
+ await page.addInitScript(
+ buildTauriMockScript({
+ httpRoutes: [
+ { pattern: "/api/v1/health", status: 200, body: { status: "ok", version: "1.0.0" } },
+ { pattern: "/api/v1/auth/login", status: 200, body: MOCK_LOGIN_2FA_RESPONSE },
+ {
+ pattern: "/api/v1/auth/verify-totp",
+ status: 401,
+ body: { error: "INVALID_CODE", message: "Invalid verification code" },
+ },
+ ],
+ }),
+ );
}
async function loginToTotp(page: import("@playwright/test").Page): Promise {
diff --git a/Client/tauri-client/tests/e2e/typing-indicator-ws.spec.ts b/Client/tauri-client/tests/e2e/typing-indicator-ws.spec.ts
index f841609a..3018c9bb 100644
--- a/Client/tauri-client/tests/e2e/typing-indicator-ws.spec.ts
+++ b/Client/tauri-client/tests/e2e/typing-indicator-ws.spec.ts
@@ -1,9 +1,5 @@
import { test, expect } from "@playwright/test";
-import {
- mockTauriFullSession,
- navigateToMainPage,
- emitWsMessage,
-} from "./helpers";
+import { mockTauriFullSession, navigateToMainPage, emitWsMessage } from "./helpers";
test.describe("Typing Indicator — WebSocket", () => {
test.beforeEach(async ({ page }) => {
@@ -18,7 +14,7 @@ test.describe("Typing Indicator — WebSocket", () => {
// Initially empty
const typingBar = page.locator(".typing-bar");
- if (await typingBar.count() > 0) {
+ if ((await typingBar.count()) > 0) {
await expect(typingBar).toBeEmpty();
}
diff --git a/Client/tauri-client/tests/e2e/typing-indicator.spec.ts b/Client/tauri-client/tests/e2e/typing-indicator.spec.ts
index 94f7dc87..a1fee5ed 100644
--- a/Client/tauri-client/tests/e2e/typing-indicator.spec.ts
+++ b/Client/tauri-client/tests/e2e/typing-indicator.spec.ts
@@ -19,7 +19,7 @@ test.describe("Typing Indicator", () => {
test("typing bar is empty by default", async ({ page }) => {
const typingBar = page.locator(".typing-bar");
- if (await typingBar.count() > 0) {
+ if ((await typingBar.count()) > 0) {
// When empty, typing bar should have no visible dots text
const text = await typingBar.textContent();
expect(text?.trim()).toBe("");
diff --git a/Client/tauri-client/tests/e2e/voice-channel.spec.ts b/Client/tauri-client/tests/e2e/voice-channel.spec.ts
index ba00ece8..97c6cc21 100644
--- a/Client/tauri-client/tests/e2e/voice-channel.spec.ts
+++ b/Client/tauri-client/tests/e2e/voice-channel.spec.ts
@@ -4,10 +4,7 @@
* VoiceWidget shows connected users when in a voice channel.
*/
import { test, expect } from "@playwright/test";
-import {
- mockTauriFullSessionWithVoice,
- navigateToMainPage,
-} from "./helpers";
+import { mockTauriFullSessionWithVoice, navigateToMainPage } from "./helpers";
test.describe("Voice Channel Items", () => {
test.beforeEach(async ({ page }) => {
diff --git a/Client/tauri-client/tests/helpers/fixtures.ts b/Client/tauri-client/tests/helpers/fixtures.ts
index aa00c2ff..420dbc55 100644
--- a/Client/tauri-client/tests/helpers/fixtures.ts
+++ b/Client/tauri-client/tests/helpers/fixtures.ts
@@ -24,9 +24,7 @@ import type {
// ---------------------------------------------------------------------------
/** Create a MessageResponse with sensible defaults. */
-export function makeMessage(
- overrides?: Partial,
-): MessageResponse {
+export function makeMessage(overrides?: Partial): MessageResponse {
return {
id: 1,
channel_id: 1,
@@ -44,9 +42,7 @@ export function makeMessage(
}
/** Create a MemberResponse with sensible defaults. */
-export function makeMember(
- overrides?: Partial,
-): MemberResponse {
+export function makeMember(overrides?: Partial): MemberResponse {
return {
id: 1,
username: "testuser",
@@ -58,9 +54,7 @@ export function makeMember(
}
/** Create a ReadyChannel with sensible defaults. */
-export function makeChannel(
- overrides?: Partial,
-): ReadyChannel {
+export function makeChannel(overrides?: Partial): ReadyChannel {
return {
id: 1,
name: "general",
@@ -74,9 +68,7 @@ export function makeChannel(
}
/** Create a ReactionSummary with sensible defaults. */
-export function makeReaction(
- overrides?: Partial,
-): ReactionSummary {
+export function makeReaction(overrides?: Partial): ReactionSummary {
return {
emoji: "👍",
count: 1,
@@ -86,9 +78,7 @@ export function makeReaction(
}
/** Create a VoiceStatePayload with sensible defaults. */
-export function makeVoiceState(
- overrides?: Partial,
-): VoiceStatePayload {
+export function makeVoiceState(overrides?: Partial): VoiceStatePayload {
return {
channel_id: 3,
user_id: 1,
@@ -107,9 +97,7 @@ export function makeVoiceState(
// ---------------------------------------------------------------------------
/** Create a MessageUser with sensible defaults. */
-export function makeMessageUser(
- overrides?: Partial,
-): MessageUser {
+export function makeMessageUser(overrides?: Partial): MessageUser {
return {
id: 1,
username: "testuser",
@@ -119,9 +107,7 @@ export function makeMessageUser(
}
/** Create an Attachment with sensible defaults. */
-export function makeAttachment(
- overrides?: Partial,
-): Attachment {
+export function makeAttachment(overrides?: Partial): Attachment {
return {
id: "att-1",
filename: "image.png",
@@ -149,9 +135,7 @@ export function makeChatMessagePayload(
}
/** Create a ReadyMember with sensible defaults. */
-export function makeReadyMember(
- overrides?: Partial,
-): ReadyMember {
+export function makeReadyMember(overrides?: Partial): ReadyMember {
return {
id: 1,
username: "testuser",
@@ -163,9 +147,7 @@ export function makeReadyMember(
}
/** Create a ReadyVoiceState with sensible defaults. */
-export function makeReadyVoiceState(
- overrides?: Partial,
-): ReadyVoiceState {
+export function makeReadyVoiceState(overrides?: Partial): ReadyVoiceState {
return {
channel_id: 3,
user_id: 1,
@@ -176,9 +158,7 @@ export function makeReadyVoiceState(
}
/** Create a ReadyRole with sensible defaults. */
-export function makeReadyRole(
- overrides?: Partial,
-): ReadyRole {
+export function makeReadyRole(overrides?: Partial): ReadyRole {
return {
id: 1,
name: "Member",
@@ -193,25 +173,41 @@ export function makeReadyRole(
// ---------------------------------------------------------------------------
/** Create a full ReadyPayload fixture for integration tests. */
-export function makeReadyPayload(
- overrides?: Partial,
-): ReadyPayload {
+export function makeReadyPayload(overrides?: Partial): ReadyPayload {
return {
channels: [
- makeChannel({ id: 1, name: "general", type: "text", position: 0, unread_count: 3, last_message_id: 100 }),
- makeChannel({ id: 2, name: "random", type: "text", position: 1, unread_count: 0, last_message_id: 50 }),
- makeChannel({ id: 3, name: "Voice Chat", type: "voice", category: "Voice Channels", position: 0 }),
+ makeChannel({
+ id: 1,
+ name: "general",
+ type: "text",
+ position: 0,
+ unread_count: 3,
+ last_message_id: 100,
+ }),
+ makeChannel({
+ id: 2,
+ name: "random",
+ type: "text",
+ position: 1,
+ unread_count: 0,
+ last_message_id: 50,
+ }),
+ makeChannel({
+ id: 3,
+ name: "Voice Chat",
+ type: "voice",
+ category: "Voice Channels",
+ position: 0,
+ }),
],
members: [
makeReadyMember({ id: 1, username: "admin", role: "admin", status: "online" }),
makeReadyMember({ id: 2, username: "user1", role: "member", status: "online" }),
],
- voice_states: [
- makeReadyVoiceState({ user_id: 1, channel_id: 3 }),
- ],
+ voice_states: [makeReadyVoiceState({ user_id: 1, channel_id: 3 })],
roles: [
- makeReadyRole({ id: 1, name: "Owner", color: "#e74c3c", permissions: 0x7FFFFFFF }),
- makeReadyRole({ id: 2, name: "Admin", color: "#f1c40f", permissions: 0x3FFFFFFF }),
+ makeReadyRole({ id: 1, name: "Owner", color: "#e74c3c", permissions: 0x7fffffff }),
+ makeReadyRole({ id: 2, name: "Admin", color: "#f1c40f", permissions: 0x3fffffff }),
makeReadyRole({ id: 3, name: "Member", color: null, permissions: 0x3 }),
],
...overrides,
diff --git a/Client/tauri-client/tests/helpers/mock-ws.ts b/Client/tauri-client/tests/helpers/mock-ws.ts
index 6117fa2b..10abe6ed 100644
--- a/Client/tauri-client/tests/helpers/mock-ws.ts
+++ b/Client/tauri-client/tests/helpers/mock-ws.ts
@@ -5,10 +5,7 @@
* requiring Tauri IPC or a real WebSocket connection.
*/
-import type {
- ServerMessage,
- ClientMessage,
-} from "@lib/types";
+import type { ServerMessage, ClientMessage } from "@lib/types";
import type { ConnectionState, WsListener, CertMismatchListener } from "@lib/ws";
interface SentEnvelope {
@@ -59,10 +56,7 @@ export function createMockWsClient() {
return id;
},
- on(
- type: T,
- listener: WsListener,
- ): () => void {
+ on(type: T, listener: WsListener): () => void {
if (!listeners.has(type)) {
listeners.set(type, new Set());
}
diff --git a/Client/tauri-client/tests/helpers/test-harness.ts b/Client/tauri-client/tests/helpers/test-harness.ts
index 6cf54a8c..bd9d67b4 100644
--- a/Client/tauri-client/tests/helpers/test-harness.ts
+++ b/Client/tauri-client/tests/helpers/test-harness.ts
@@ -75,9 +75,7 @@ export function createTestHarness(): TestHarness {
click(selector: string): void {
const el = container.querySelector(selector) as HTMLElement | null;
if (el === null) {
- throw new Error(
- `click("${selector}"): no element found in container`,
- );
+ throw new Error(`click("${selector}"): no element found in container`);
}
el.click();
},
diff --git a/Client/tauri-client/tests/helpers/test-utils.ts b/Client/tauri-client/tests/helpers/test-utils.ts
index f2fa2b28..0e910a6a 100644
--- a/Client/tauri-client/tests/helpers/test-utils.ts
+++ b/Client/tauri-client/tests/helpers/test-utils.ts
@@ -57,7 +57,7 @@ const VOICE_INITIAL: VoiceState = {
localCamera: false,
localScreenshare: false,
joinedAt: null,
- listenOnly: false,
+ listenOnly: false,
};
const UI_INITIAL: UiState = {
diff --git a/Client/tauri-client/tests/integration/stores.test.ts b/Client/tauri-client/tests/integration/stores.test.ts
index b57eac10..0be0c00d 100644
--- a/Client/tauri-client/tests/integration/stores.test.ts
+++ b/Client/tauri-client/tests/integration/stores.test.ts
@@ -46,10 +46,7 @@ function createMockWsClient(): MockWsClient {
return crypto.randomUUID();
},
- on(
- type: T,
- listener: WsListener,
- ): () => void {
+ on(type: T, listener: WsListener): () => void {
if (!listeners.has(type)) {
listeners.set(type, new Set());
}
@@ -166,8 +163,24 @@ describe("Store integration via dispatcher", () => {
it("populates channels, members, and voice stores from ready event", () => {
ws.simulate("ready", {
channels: [
- { id: 1, name: "general", type: "text", category: "Text Channels", position: 0, unread_count: 3, last_message_id: 100 },
- { id: 2, name: "random", type: "text", category: "Text Channels", position: 1, unread_count: 0, last_message_id: 50 },
+ {
+ id: 1,
+ name: "general",
+ type: "text",
+ category: "Text Channels",
+ position: 0,
+ unread_count: 3,
+ last_message_id: 100,
+ },
+ {
+ id: 2,
+ name: "random",
+ type: "text",
+ category: "Text Channels",
+ position: 1,
+ unread_count: 0,
+ last_message_id: 50,
+ },
{ id: 3, name: "Voice Chat", type: "voice", category: "Voice Channels", position: 0 },
],
members: [
@@ -180,7 +193,7 @@ describe("Store integration via dispatcher", () => {
{ channel_id: 3, user_id: 2, muted: true, deafened: false },
],
roles: [
- { id: 1, name: "Admin", color: "#f1c40f", permissions: 0x3FFFFFFF },
+ { id: 1, name: "Admin", color: "#f1c40f", permissions: 0x3fffffff },
{ id: 2, name: "Member", color: null, permissions: 0x3 },
],
});
@@ -222,9 +235,7 @@ describe("Store integration via dispatcher", () => {
{ id: 1, name: "general", type: "text", category: null, position: 0, unread_count: 0 },
{ id: 2, name: "random", type: "text", category: null, position: 1, unread_count: 0 },
],
- members: [
- { id: 10, username: "sender", avatar: null, role: "member", status: "online" },
- ],
+ members: [{ id: 10, username: "sender", avatar: null, role: "member", status: "online" }],
voice_states: [],
roles: [],
});
@@ -435,10 +446,7 @@ describe("Store integration via dispatcher", () => {
addPendingSend(correlationId, 1);
// Simulate without an id
- ws.simulate(
- "chat_send_ok",
- { message_id: 501, timestamp: "2026-03-15T13:01:00Z" },
- );
+ ws.simulate("chat_send_ok", { message_id: 501, timestamp: "2026-03-15T13:01:00Z" });
// Pending send remains because no correlation ID was provided
expect(messagesStore.getState().pendingSends.has(correlationId)).toBe(true);
diff --git a/Client/tauri-client/tests/unit/admin-actions.test.ts b/Client/tauri-client/tests/unit/admin-actions.test.ts
index 4a05927f..29c654e2 100644
--- a/Client/tauri-client/tests/unit/admin-actions.test.ts
+++ b/Client/tauri-client/tests/unit/admin-actions.test.ts
@@ -1,12 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import {
- createMemberContextMenu,
- createChannelContextMenu,
-} from "@components/AdminActions";
-import type {
- MemberContextMenuOptions,
- ChannelContextMenuOptions,
-} from "@components/AdminActions";
+import { createMemberContextMenu, createChannelContextMenu } from "@components/AdminActions";
+import type { MemberContextMenuOptions, ChannelContextMenuOptions } from "@components/AdminActions";
describe("AdminActions", () => {
let container: HTMLDivElement;
@@ -86,7 +80,9 @@ describe("AdminActions", () => {
const { result } = makeMenu({ onKick });
const dangerItems = result.element.querySelectorAll(".context-menu__item--danger");
- const kickItem = Array.from(dangerItems).find((i) => i.textContent === "Kick") as HTMLDivElement;
+ const kickItem = Array.from(dangerItems).find(
+ (i) => i.textContent === "Kick",
+ ) as HTMLDivElement;
// First click changes text to confirmation
kickItem.click();
@@ -104,7 +100,9 @@ describe("AdminActions", () => {
const { result } = makeMenu({ onBan });
const dangerItems = result.element.querySelectorAll(".context-menu__item--danger");
- const banItem = Array.from(dangerItems).find((i) => i.textContent === "Ban") as HTMLDivElement;
+ const banItem = Array.from(dangerItems).find(
+ (i) => i.textContent === "Ban",
+ ) as HTMLDivElement;
banItem.click();
expect(banItem.textContent).toBe("Are you sure?");
@@ -165,7 +163,9 @@ describe("AdminActions", () => {
const { result } = makeMenu({ onEdit });
const items = result.element.querySelectorAll(".context-menu__item");
- const editItem = Array.from(items).find((i) => i.textContent === "Edit Channel") as HTMLDivElement;
+ const editItem = Array.from(items).find(
+ (i) => i.textContent === "Edit Channel",
+ ) as HTMLDivElement;
editItem.click();
expect(onEdit).toHaveBeenCalledOnce();
@@ -177,7 +177,9 @@ describe("AdminActions", () => {
const { result } = makeMenu({ onCreate });
const items = result.element.querySelectorAll(".context-menu__item");
- const createItem = Array.from(items).find((i) => i.textContent === "Create Channel") as HTMLDivElement;
+ const createItem = Array.from(items).find(
+ (i) => i.textContent === "Create Channel",
+ ) as HTMLDivElement;
createItem.click();
expect(onCreate).toHaveBeenCalledOnce();
diff --git a/Client/tauri-client/tests/unit/advanced-tab.test.ts b/Client/tauri-client/tests/unit/advanced-tab.test.ts
index ed05b439..625a1d60 100644
--- a/Client/tauri-client/tests/unit/advanced-tab.test.ts
+++ b/Client/tauri-client/tests/unit/advanced-tab.test.ts
@@ -17,7 +17,15 @@ const {
mockClearAttachmentCaches: vi.fn(),
mockClearEmbedCaches: vi.fn(),
mockClearMediaCaches: vi.fn(),
- deleteDbState: { mode: "success" as "success" | "blocked-then-success" | "blocked-stuck" | "error" | "blocked-double" | "success-then-blocked" },
+ deleteDbState: {
+ mode: "success" as
+ | "success"
+ | "blocked-then-success"
+ | "blocked-stuck"
+ | "error"
+ | "blocked-double"
+ | "success-then-blocked",
+ },
}));
// Mock Tauri APIs
@@ -152,9 +160,7 @@ describe("AdvancedTab — Clear All Cache", () => {
const section = buildAdvancedTab(ac.signal);
container.appendChild(section);
const buttons = container.querySelectorAll("button.ac-btn");
- const btn = Array.from(buttons).find(
- (b) => b.textContent === label,
- ) as HTMLButtonElement;
+ const btn = Array.from(buttons).find((b) => b.textContent === label) as HTMLButtonElement;
expect(btn).toBeDefined();
return btn;
}
@@ -166,7 +172,10 @@ describe("AdvancedTab — Clear All Cache", () => {
}
it("preserves owncord:profiles after Clear All Cache", async () => {
- localStorage.setItem("owncord:profiles", JSON.stringify([{ name: "Local", host: "localhost" }]));
+ localStorage.setItem(
+ "owncord:profiles",
+ JSON.stringify([{ name: "Local", host: "localhost" }]),
+ );
localStorage.setItem("owncord:settings:fontSize", "16");
sessionStorage.setItem("some-session-key", "value");
@@ -204,7 +213,10 @@ describe("AdvancedTab — Clear All Cache", () => {
it("preserves active and custom theme keys after Clear All Cache", async () => {
localStorage.setItem("owncord:theme:active", "custom-sunrise");
- localStorage.setItem("owncord:theme:custom:custom-sunrise", JSON.stringify({ name: "custom-sunrise" }));
+ localStorage.setItem(
+ "owncord:theme:custom:custom-sunrise",
+ JSON.stringify({ name: "custom-sunrise" }),
+ );
localStorage.setItem("owncord:settings:accentColor", '"#00c8ff"');
const btn = getClearAllBtn();
@@ -470,7 +482,9 @@ describe("AdvancedTab — Toggles & Structure", () => {
const section = buildAdvancedTab(ac.signal);
container.appendChild(section);
- const labels = Array.from(container.querySelectorAll(".setting-label")).map((l) => l.textContent);
+ const labels = Array.from(container.querySelectorAll(".setting-label")).map(
+ (l) => l.textContent,
+ );
expect(labels).toContain("Clear Image Cache");
expect(labels).toContain("Clear Log Files");
expect(labels).toContain("Clear All Cache & Restart");
diff --git a/Client/tauri-client/tests/unit/appearance-tab.test.ts b/Client/tauri-client/tests/unit/appearance-tab.test.ts
index 0aedecb5..0a1947f7 100644
--- a/Client/tauri-client/tests/unit/appearance-tab.test.ts
+++ b/Client/tauri-client/tests/unit/appearance-tab.test.ts
@@ -1,17 +1,20 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { buildAppearanceTab } from "@components/settings/AppearanceTab";
-const {
- mockGetActiveThemeName,
- mockLoadCustomTheme,
- mockRestoreTheme,
- mockApplyThemeByName,
-} = vi.hoisted(() => ({
- mockGetActiveThemeName: vi.fn(() => "neon-glow"),
- mockLoadCustomTheme: vi.fn((): { name: string; author: string; version: string; colors: Record } | null => null),
- mockRestoreTheme: vi.fn(),
- mockApplyThemeByName: vi.fn(),
-}));
+const { mockGetActiveThemeName, mockLoadCustomTheme, mockRestoreTheme, mockApplyThemeByName } =
+ vi.hoisted(() => ({
+ mockGetActiveThemeName: vi.fn(() => "neon-glow"),
+ mockLoadCustomTheme: vi.fn(
+ (): {
+ name: string;
+ author: string;
+ version: string;
+ colors: Record;
+ } | null => null,
+ ),
+ mockRestoreTheme: vi.fn(),
+ mockApplyThemeByName: vi.fn(),
+ }));
vi.mock("@stores/ui.store", () => ({
setTheme: vi.fn(),
@@ -109,8 +112,8 @@ describe("AppearanceTab — Accessibility", () => {
const section = buildAppearanceTab(ac.signal);
container.appendChild(section);
- const activeSwatch = container.querySelector('.accent-swatch.active') as HTMLElement;
- const hexInput = container.querySelector('.accent-hex-row input') as HTMLInputElement;
+ const activeSwatch = container.querySelector(".accent-swatch.active") as HTMLElement;
+ const hexInput = container.querySelector(".accent-hex-row input") as HTMLInputElement;
expect(activeSwatch).not.toBeNull();
expect(activeSwatch.title).toBe("#00c8ff");
@@ -123,8 +126,8 @@ describe("AppearanceTab — Accessibility", () => {
const section = buildAppearanceTab(ac.signal);
container.appendChild(section);
- const activeSwatch = container.querySelector('.accent-swatch.active') as HTMLElement;
- const hexInput = container.querySelector('.accent-hex-row input') as HTMLInputElement;
+ const activeSwatch = container.querySelector(".accent-swatch.active") as HTMLElement;
+ const hexInput = container.querySelector(".accent-hex-row input") as HTMLInputElement;
expect(activeSwatch).not.toBeNull();
expect(activeSwatch.title).toBe("#5865f2");
@@ -143,8 +146,8 @@ describe("AppearanceTab — Accessibility", () => {
const section = buildAppearanceTab(ac.signal);
container.appendChild(section);
- const activeSwatch = container.querySelector('.accent-swatch.active');
- const hexInput = container.querySelector('.accent-hex-row input') as HTMLInputElement;
+ const activeSwatch = container.querySelector(".accent-swatch.active");
+ const hexInput = container.querySelector(".accent-hex-row input") as HTMLInputElement;
expect(activeSwatch).toBeNull();
expect(hexInput.value).toBe("123456");
@@ -157,11 +160,11 @@ describe("AppearanceTab — Accessibility", () => {
const tiles = container.querySelectorAll(".theme-opt");
const dark = tiles[0] as HTMLElement;
- const hexInput = container.querySelector('.accent-hex-row input') as HTMLInputElement;
+ const hexInput = container.querySelector(".accent-hex-row input") as HTMLInputElement;
dark.click();
- const activeSwatch = container.querySelector('.accent-swatch.active') as HTMLElement;
+ const activeSwatch = container.querySelector(".accent-swatch.active") as HTMLElement;
expect(activeSwatch.title).toBe("#5865f2");
expect(hexInput.value).toBe("5865f2");
expect(hexInput.placeholder).toBe("5865f2");
diff --git a/Client/tauri-client/tests/unit/attachments-cache.test.ts b/Client/tauri-client/tests/unit/attachments-cache.test.ts
index 2b16c0bd..7161240e 100644
--- a/Client/tauri-client/tests/unit/attachments-cache.test.ts
+++ b/Client/tauri-client/tests/unit/attachments-cache.test.ts
@@ -32,7 +32,11 @@ vi.stubGlobal("indexedDB", {
onerror: null,
objectStore: () => ({
get: () => {
- const req: Record = { onsuccess: null, onerror: null, result: undefined };
+ const req: Record = {
+ onsuccess: null,
+ onerror: null,
+ result: undefined,
+ };
Promise.resolve().then(() => {
const fn = req.onsuccess as ((ev: Event) => void) | null;
fn?.(new Event("success"));
@@ -68,7 +72,11 @@ vi.stubGlobal("indexedDB", {
},
});
-import { clearAttachmentCaches, fetchImageAsDataUrl, renderAttachment } from "../../src/components/message-list/attachments";
+import {
+ clearAttachmentCaches,
+ fetchImageAsDataUrl,
+ renderAttachment,
+} from "../../src/components/message-list/attachments";
function imageResponse() {
return {
@@ -88,9 +96,12 @@ describe("attachment cache clearing", () => {
it("does not repopulate caches from an in-flight fetch after clear", async () => {
let resolveFetch: ((value: ReturnType) => void) | undefined;
- fetchMock.mockImplementationOnce(() => new Promise((resolve) => {
- resolveFetch = resolve;
- }));
+ fetchMock.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
+ );
const pending = fetchImageAsDataUrl("https://example.com/image.png");
await vi.waitFor(() => {
@@ -111,12 +122,18 @@ describe("attachment cache clearing", () => {
let resolveFirst: ((value: ReturnType) => void) | undefined;
let resolveSecond: ((value: ReturnType) => void) | undefined;
fetchMock
- .mockImplementationOnce(() => new Promise((resolve) => {
- resolveFirst = resolve;
- }))
- .mockImplementationOnce(() => new Promise((resolve) => {
- resolveSecond = resolve;
- }));
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveFirst = resolve;
+ }),
+ )
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveSecond = resolve;
+ }),
+ );
const first = fetchImageAsDataUrl("https://example.com/image.png");
await vi.waitFor(() => {
@@ -142,9 +159,12 @@ describe("attachment cache clearing", () => {
it("stops showing a loading placeholder when a mid-fetch clear invalidates the result", async () => {
let resolveFetch: ((value: ReturnType) => void) | undefined;
- fetchMock.mockImplementationOnce(() => new Promise((resolve) => {
- resolveFetch = resolve;
- }));
+ fetchMock.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
+ );
const element = renderAttachment({
id: "att-1",
@@ -169,4 +189,4 @@ describe("attachment cache clearing", () => {
expect(placeholder.classList.contains("loading")).toBe(false);
});
});
-});
\ No newline at end of file
+});
diff --git a/Client/tauri-client/tests/unit/attachments-render.test.ts b/Client/tauri-client/tests/unit/attachments-render.test.ts
index 30bbfe36..8351523f 100644
--- a/Client/tauri-client/tests/unit/attachments-render.test.ts
+++ b/Client/tauri-client/tests/unit/attachments-render.test.ts
@@ -37,10 +37,16 @@ vi.stubGlobal("indexedDB", {
close: vi.fn(),
transaction: () => {
const tx: Record = {
- oncomplete: null, onabort: null, onerror: null,
+ oncomplete: null,
+ onabort: null,
+ onerror: null,
objectStore: () => ({
get: () => {
- const req: Record = { onsuccess: null, onerror: null, result: undefined };
+ const req: Record = {
+ onsuccess: null,
+ onerror: null,
+ result: undefined,
+ };
Promise.resolve().then(() => {
const fn = req.onsuccess as ((ev: Event) => void) | null;
fn?.(new Event("success"));
@@ -59,7 +65,10 @@ vi.stubGlobal("indexedDB", {
};
const req: Record = {
- result: db, onsuccess: null, onerror: null, onupgradeneeded: null,
+ result: db,
+ onsuccess: null,
+ onerror: null,
+ onupgradeneeded: null,
};
Promise.resolve().then(() => {
const upgrade = req.onupgradeneeded as ((ev: Event) => void) | null;
@@ -95,11 +104,15 @@ describe("resolveServerUrl", () => {
});
it("returns absolute https URLs unchanged", () => {
- expect(resolveServerUrl("https://cdn.example.com/file.png")).toBe("https://cdn.example.com/file.png");
+ expect(resolveServerUrl("https://cdn.example.com/file.png")).toBe(
+ "https://cdn.example.com/file.png",
+ );
});
it("prepends server host for relative paths", () => {
- expect(resolveServerUrl("/api/v1/attachments/1.png")).toBe("https://myserver.local:8443/api/v1/attachments/1.png");
+ expect(resolveServerUrl("/api/v1/attachments/1.png")).toBe(
+ "https://myserver.local:8443/api/v1/attachments/1.png",
+ );
});
it("returns the path as-is when no server host is set", () => {
diff --git a/Client/tauri-client/tests/unit/audio-elements.test.ts b/Client/tauri-client/tests/unit/audio-elements.test.ts
index 615e7574..8dc68c60 100644
--- a/Client/tauri-client/tests/unit/audio-elements.test.ts
+++ b/Client/tauri-client/tests/unit/audio-elements.test.ts
@@ -507,9 +507,15 @@ describe("AudioElements", () => {
expect(removeSs).toHaveBeenCalled();
});
- it("clears muted-by-user state", () => {
+ it("preserves muted-by-user state for reconnecting tracks", () => {
elements.muteScreenshareAudio(42, true);
elements.cleanupAllAudioElements();
+ expect(elements.getScreenshareAudioMuted(42)).toBe(true);
+ });
+
+ it("clears muted-by-user state with full cleanup", () => {
+ elements.muteScreenshareAudio(42, true);
+ elements.cleanupAllAudioElementsFull();
expect(elements.getScreenshareAudioMuted(42)).toBe(false);
});
});
diff --git a/Client/tauri-client/tests/unit/audio-pipeline.test.ts b/Client/tauri-client/tests/unit/audio-pipeline.test.ts
index 3286d407..c05b6faf 100644
--- a/Client/tauri-client/tests/unit/audio-pipeline.test.ts
+++ b/Client/tauri-client/tests/unit/audio-pipeline.test.ts
@@ -357,7 +357,10 @@ describe("AudioPipeline", () => {
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
- vi.stubGlobal("MediaStream", vi.fn().mockImplementation(() => ({})));
+ vi.stubGlobal(
+ "MediaStream",
+ vi.fn().mockImplementation(() => ({})),
+ );
mockRoom = {
localParticipant: {
@@ -508,9 +511,10 @@ describe("AudioPipeline", () => {
pipeline.setInputVolume(50);
expect(mockGainNode.gain.setTargetAtTime).toHaveBeenCalled();
- const call = mockGainNode.gain.setTargetAtTime.mock.calls[
- mockGainNode.gain.setTargetAtTime.mock.calls.length - 1
- ];
+ const call =
+ mockGainNode.gain.setTargetAtTime.mock.calls[
+ mockGainNode.gain.setTargetAtTime.mock.calls.length - 1
+ ];
expect(call[0]).toBe(0.5); // inputGain = 50/100 = 0.5, not vadGated
});
@@ -521,14 +525,20 @@ describe("AudioPipeline", () => {
(pipeline as any).vadGated = true;
pipeline.updatePipelineGain();
- const call = mockGainNode.gain.setTargetAtTime.mock.calls[
- mockGainNode.gain.setTargetAtTime.mock.calls.length - 1
- ];
+ const call =
+ mockGainNode.gain.setTargetAtTime.mock.calls[
+ mockGainNode.gain.setTargetAtTime.mock.calls.length - 1
+ ];
expect(call[0]).toBe(0);
});
it("handles AudioContext constructor failure gracefully", () => {
- vi.stubGlobal("AudioContext", vi.fn(() => { throw new Error("AudioContext not supported"); }));
+ vi.stubGlobal(
+ "AudioContext",
+ vi.fn(() => {
+ throw new Error("AudioContext not supported");
+ }),
+ );
pipeline.setRoom(mockRoom);
// Should not throw
expect(() => pipeline.setupAudioPipeline()).not.toThrow();
@@ -552,7 +562,9 @@ describe("AudioPipeline", () => {
} as any;
pipeline.setRoom(mockRoom);
await pipeline.applyNoiseSuppressor();
- expect(mockRoom.localParticipant.getTrackPublication().track.setProcessor).not.toHaveBeenCalled();
+ expect(
+ mockRoom.localParticipant.getTrackPublication().track.setProcessor,
+ ).not.toHaveBeenCalled();
});
it("attaches processor when track has none", async () => {
@@ -587,7 +599,9 @@ describe("AudioPipeline", () => {
} as any;
pipeline.setRoom(mockRoom);
await pipeline.removeNoiseSuppressor();
- expect(mockRoom.localParticipant.getTrackPublication().track.stopProcessor).not.toHaveBeenCalled();
+ expect(
+ mockRoom.localParticipant.getTrackPublication().track.stopProcessor,
+ ).not.toHaveBeenCalled();
});
it("removes processor when track has one", async () => {
@@ -648,11 +662,14 @@ describe("AudioPipeline", () => {
function setupPipelineWithWorklet(workletBehavior: "success" | "fail"): void {
mockGainNode = {
gain: { value: 1, setValueAtTime: vi.fn(), setTargetAtTime: vi.fn() },
- connect: vi.fn(), disconnect: vi.fn(),
+ connect: vi.fn(),
+ disconnect: vi.fn(),
};
mockAnalyserNode = {
- fftSize: 0, smoothingTimeConstant: 0,
- connect: vi.fn(), disconnect: vi.fn(),
+ fftSize: 0,
+ smoothingTimeConstant: 0,
+ connect: vi.fn(),
+ disconnect: vi.fn(),
getFloatTimeDomainData: vi.fn(),
};
mockDestNode = {
@@ -670,23 +687,30 @@ describe("AudioPipeline", () => {
close: vi.fn().mockResolvedValue(undefined),
state: "running",
audioWorklet: {
- addModule: workletBehavior === "success"
- ? vi.fn().mockResolvedValue(undefined)
- : vi.fn().mockRejectedValue(new Error("no worklet")),
+ addModule:
+ workletBehavior === "success"
+ ? vi.fn().mockResolvedValue(undefined)
+ : vi.fn().mockRejectedValue(new Error("no worklet")),
},
};
// 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.stubGlobal(
+ "AudioWorkletNode",
+ vi.fn().mockImplementation(() => ({
+ port: {
+ postMessage: vi.fn(),
+ onmessage: null as ((event: MessageEvent) => void) | null,
+ },
+ connect: vi.fn(),
+ disconnect: vi.fn(),
+ })),
+ );
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
- vi.stubGlobal("MediaStream", vi.fn().mockImplementation(() => ({})));
+ vi.stubGlobal(
+ "MediaStream",
+ vi.fn().mockImplementation(() => ({})),
+ );
mockRoom = {
localParticipant: {
@@ -694,7 +718,9 @@ describe("AudioPipeline", () => {
track: {
mediaStreamTrack: { id: "track" },
sender: { replaceTrack: vi.fn().mockResolvedValue(undefined) },
- getProcessor: vi.fn(), setProcessor: vi.fn(), stopProcessor: vi.fn(),
+ getProcessor: vi.fn(),
+ setProcessor: vi.fn(),
+ stopProcessor: vi.fn(),
},
}),
},
@@ -789,9 +815,12 @@ describe("AudioPipeline", () => {
it("falls back to setTimeout when AudioWorkletNode constructor throws", async () => {
setupPipelineWithWorklet("success");
// Override AudioWorkletNode to throw
- vi.stubGlobal("AudioWorkletNode", vi.fn().mockImplementation(() => {
- throw new Error("AudioWorkletNode not supported");
- }));
+ vi.stubGlobal(
+ "AudioWorkletNode",
+ vi.fn().mockImplementation(() => {
+ throw new Error("AudioWorkletNode not supported");
+ }),
+ );
pipeline.setRoom(mockRoom);
pipeline.setupAudioPipeline();
@@ -819,15 +848,18 @@ describe("AudioPipeline", () => {
dataArray.fill(0);
const mockAnalyser = {
- fftSize: 2048, smoothingTimeConstant: 0.3,
- connect: vi.fn(), disconnect: vi.fn(),
+ fftSize: 2048,
+ smoothingTimeConstant: 0.3,
+ connect: vi.fn(),
+ disconnect: vi.fn(),
getFloatTimeDomainData: vi.fn().mockImplementation((arr: Float32Array) => {
arr.set(dataArray);
}),
};
const mockGainNode = {
gain: { value: 1, setValueAtTime: vi.fn(), setTargetAtTime: vi.fn() },
- connect: vi.fn(), disconnect: vi.fn(),
+ connect: vi.fn(),
+ disconnect: vi.fn(),
};
const mockAudioCtx = {
resume: vi.fn().mockResolvedValue(undefined),
@@ -845,7 +877,10 @@ describe("AudioPipeline", () => {
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
- vi.stubGlobal("MediaStream", vi.fn().mockImplementation(() => ({})));
+ vi.stubGlobal(
+ "MediaStream",
+ vi.fn().mockImplementation(() => ({})),
+ );
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "voiceSensitivity") return 50;
@@ -859,7 +894,9 @@ describe("AudioPipeline", () => {
track: {
mediaStreamTrack: { id: "track" },
sender: { replaceTrack: vi.fn().mockResolvedValue(undefined) },
- getProcessor: vi.fn(), setProcessor: vi.fn(), stopProcessor: vi.fn(),
+ getProcessor: vi.fn(),
+ setProcessor: vi.fn(),
+ stopProcessor: vi.fn(),
},
}),
},
@@ -882,8 +919,10 @@ describe("AudioPipeline", () => {
vi.useFakeTimers();
let isSilent = true;
const mockAnalyser = {
- fftSize: 2048, smoothingTimeConstant: 0.3,
- connect: vi.fn(), disconnect: vi.fn(),
+ fftSize: 2048,
+ smoothingTimeConstant: 0.3,
+ connect: vi.fn(),
+ disconnect: vi.fn(),
getFloatTimeDomainData: vi.fn().mockImplementation((arr: Float32Array) => {
if (isSilent) {
arr.fill(0);
@@ -895,7 +934,8 @@ describe("AudioPipeline", () => {
};
const mockGainNode = {
gain: { value: 1, setValueAtTime: vi.fn(), setTargetAtTime: vi.fn() },
- connect: vi.fn(), disconnect: vi.fn(),
+ connect: vi.fn(),
+ disconnect: vi.fn(),
};
const mockAudioCtx = {
resume: vi.fn().mockResolvedValue(undefined),
@@ -913,7 +953,10 @@ describe("AudioPipeline", () => {
};
vi.stubGlobal("AudioContext", vi.fn().mockReturnValue(mockAudioCtx));
- vi.stubGlobal("MediaStream", vi.fn().mockImplementation(() => ({})));
+ vi.stubGlobal(
+ "MediaStream",
+ vi.fn().mockImplementation(() => ({})),
+ );
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "voiceSensitivity") return 50;
@@ -927,7 +970,9 @@ describe("AudioPipeline", () => {
track: {
mediaStreamTrack: { id: "track" },
sender: { replaceTrack: vi.fn().mockResolvedValue(undefined) },
- getProcessor: vi.fn(), setProcessor: vi.fn(), stopProcessor: vi.fn(),
+ getProcessor: vi.fn(),
+ setProcessor: vi.fn(),
+ stopProcessor: vi.fn(),
},
}),
},
@@ -975,27 +1020,37 @@ describe("AudioPipeline", () => {
} as any;
// Stub AudioContext for setupAudioPipeline called internally
- vi.stubGlobal("AudioContext", vi.fn().mockReturnValue({
- resume: vi.fn().mockResolvedValue(undefined),
- createMediaStreamSource: vi.fn().mockReturnValue({ connect: vi.fn() }),
- createAnalyser: vi.fn().mockReturnValue({
- fftSize: 0, smoothingTimeConstant: 0, connect: vi.fn(), disconnect: vi.fn(),
- getFloatTimeDomainData: vi.fn(),
+ vi.stubGlobal(
+ "AudioContext",
+ vi.fn().mockReturnValue({
+ resume: vi.fn().mockResolvedValue(undefined),
+ createMediaStreamSource: vi.fn().mockReturnValue({ connect: vi.fn() }),
+ createAnalyser: vi.fn().mockReturnValue({
+ fftSize: 0,
+ smoothingTimeConstant: 0,
+ connect: vi.fn(),
+ disconnect: vi.fn(),
+ getFloatTimeDomainData: vi.fn(),
+ }),
+ createGain: vi.fn().mockReturnValue({
+ gain: { value: 1, setValueAtTime: vi.fn(), setTargetAtTime: vi.fn() },
+ connect: vi.fn(),
+ disconnect: vi.fn(),
+ }),
+ createMediaStreamDestination: vi.fn().mockReturnValue({
+ stream: { getAudioTracks: vi.fn().mockReturnValue([]) },
+ disconnect: vi.fn(),
+ }),
+ currentTime: 0,
+ close: vi.fn().mockResolvedValue(undefined),
+ state: "running",
+ audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
}),
- createGain: vi.fn().mockReturnValue({
- gain: { value: 1, setValueAtTime: vi.fn(), setTargetAtTime: vi.fn() },
- connect: vi.fn(), disconnect: vi.fn(),
- }),
- createMediaStreamDestination: vi.fn().mockReturnValue({
- stream: { getAudioTracks: vi.fn().mockReturnValue([]) },
- disconnect: vi.fn(),
- }),
- currentTime: 0,
- close: vi.fn().mockResolvedValue(undefined),
- state: "running",
- audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
- }));
- vi.stubGlobal("MediaStream", vi.fn().mockImplementation(() => ({})));
+ );
+ vi.stubGlobal(
+ "MediaStream",
+ vi.fn().mockImplementation(() => ({})),
+ );
pipeline.setRoom(mockRoom);
await pipeline.reapplyAudioProcessing();
@@ -1033,27 +1088,37 @@ describe("AudioPipeline", () => {
},
} as any;
- vi.stubGlobal("AudioContext", vi.fn().mockReturnValue({
- resume: vi.fn().mockResolvedValue(undefined),
- createMediaStreamSource: vi.fn().mockReturnValue({ connect: vi.fn() }),
- createAnalyser: vi.fn().mockReturnValue({
- fftSize: 0, smoothingTimeConstant: 0, connect: vi.fn(), disconnect: vi.fn(),
- getFloatTimeDomainData: vi.fn(),
+ vi.stubGlobal(
+ "AudioContext",
+ vi.fn().mockReturnValue({
+ resume: vi.fn().mockResolvedValue(undefined),
+ createMediaStreamSource: vi.fn().mockReturnValue({ connect: vi.fn() }),
+ createAnalyser: vi.fn().mockReturnValue({
+ fftSize: 0,
+ smoothingTimeConstant: 0,
+ connect: vi.fn(),
+ disconnect: vi.fn(),
+ getFloatTimeDomainData: vi.fn(),
+ }),
+ createGain: vi.fn().mockReturnValue({
+ gain: { value: 1, setValueAtTime: vi.fn(), setTargetAtTime: vi.fn() },
+ connect: vi.fn(),
+ disconnect: vi.fn(),
+ }),
+ createMediaStreamDestination: vi.fn().mockReturnValue({
+ stream: { getAudioTracks: vi.fn().mockReturnValue([]) },
+ disconnect: vi.fn(),
+ }),
+ currentTime: 0,
+ close: vi.fn().mockResolvedValue(undefined),
+ state: "running",
+ audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
}),
- createGain: vi.fn().mockReturnValue({
- gain: { value: 1, setValueAtTime: vi.fn(), setTargetAtTime: vi.fn() },
- connect: vi.fn(), disconnect: vi.fn(),
- }),
- createMediaStreamDestination: vi.fn().mockReturnValue({
- stream: { getAudioTracks: vi.fn().mockReturnValue([]) },
- disconnect: vi.fn(),
- }),
- currentTime: 0,
- close: vi.fn().mockResolvedValue(undefined),
- state: "running",
- audioWorklet: { addModule: vi.fn().mockRejectedValue(new Error("no worklet")) },
- }));
- vi.stubGlobal("MediaStream", vi.fn().mockImplementation(() => ({})));
+ );
+ vi.stubGlobal(
+ "MediaStream",
+ vi.fn().mockImplementation(() => ({})),
+ );
pipeline.setRoom(mockRoom);
await pipeline.reapplyAudioProcessing();
diff --git a/Client/tauri-client/tests/unit/cert-mismatch-modal.test.ts b/Client/tauri-client/tests/unit/cert-mismatch-modal.test.ts
index 341e74d4..85538869 100644
--- a/Client/tauri-client/tests/unit/cert-mismatch-modal.test.ts
+++ b/Client/tauri-client/tests/unit/cert-mismatch-modal.test.ts
@@ -13,9 +13,7 @@ describe("parseStoredFingerprint", () => {
"Stored: 51:32:d1:f9:61:47:e4:cc:26:6f:3a:87\n" +
"Current: 23:e4:00:61:11:f7:e5:12:eb:b9:2d:19\n" +
"This may indicate a man-in-the-middle attack.";
- expect(parseStoredFingerprint(msg)).toBe(
- "51:32:d1:f9:61:47:e4:cc:26:6f:3a:87",
- );
+ expect(parseStoredFingerprint(msg)).toBe("51:32:d1:f9:61:47:e4:cc:26:6f:3a:87");
});
it("returns undefined for undefined message", () => {
diff --git a/Client/tauri-client/tests/unit/channel-controller.test.ts b/Client/tauri-client/tests/unit/channel-controller.test.ts
index 83a58f0b..4ebfa789 100644
--- a/Client/tauri-client/tests/unit/channel-controller.test.ts
+++ b/Client/tauri-client/tests/unit/channel-controller.test.ts
@@ -22,7 +22,14 @@ const {
mockMessageInputDestroy: vi.fn(),
mockTypingMount: vi.fn(),
mockTypingDestroy: vi.fn(),
- mockGetChannelMessages: vi.fn((): Array<{ id: number; content?: string; user?: { id: number; username: string }; deleted?: boolean }> => []),
+ mockGetChannelMessages: vi.fn(
+ (): Array<{
+ id: number;
+ content?: string;
+ user?: { id: number; username: string };
+ deleted?: boolean;
+ }> => [],
+ ),
mockSetReplyTo: vi.fn(),
mockStartEdit: vi.fn(),
mockScrollToMessage: vi.fn(() => true),
@@ -30,14 +37,21 @@ const {
vi.mock("@lib/logger", () => ({
createLogger: () => ({
- debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(),
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
}),
}));
vi.mock("@lib/dom", () => ({
createElement: vi.fn((tag: string) => document.createElement(tag)),
- clearChildren: vi.fn((el: HTMLElement) => { el.innerHTML = ""; }),
- setText: vi.fn((el: HTMLElement, text: string) => { el.textContent = text; }),
+ clearChildren: vi.fn((el: HTMLElement) => {
+ el.innerHTML = "";
+ }),
+ setText: vi.fn((el: HTMLElement, text: string) => {
+ el.textContent = text;
+ }),
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- captured from mock factory, typed at call sites
@@ -93,7 +107,16 @@ vi.mock("../../src/pages/main-page/ChatHeader", () => ({
}));
const { mockDmStoreGetState, mockMembersStoreGetState } = vi.hoisted(() => ({
- mockDmStoreGetState: vi.fn(() => ({ channels: [] as Array<{ channelId: number; recipient: { id: number; username: string; avatar: string; status: string }; lastMessageId: number | null; lastMessage: string; lastMessageAt: string; unreadCount: number }> })),
+ mockDmStoreGetState: vi.fn(() => ({
+ channels: [] as Array<{
+ channelId: number;
+ recipient: { id: number; username: string; avatar: string; status: string };
+ lastMessageId: number | null;
+ lastMessage: string;
+ lastMessageAt: string;
+ unreadCount: number;
+ }>,
+ })),
mockMembersStoreGetState: vi.fn(() => ({ members: new Map() })),
}));
@@ -126,11 +149,22 @@ function makeSlots(): ChannelControllerOptions["slots"] {
function makeOpts(overrides: Partial = {}): ChannelControllerOptions {
return {
- ws: { send: vi.fn(), getState: vi.fn(() => "connected") } as unknown as ChannelControllerOptions["ws"],
- api: { uploadFile: vi.fn().mockResolvedValue({ id: 1, url: "/f/1", filename: "f.txt" }) } as unknown as ChannelControllerOptions["api"],
- msgCtrl: { loadMessages: vi.fn(), loadOlderMessages: vi.fn() } as unknown as ChannelControllerOptions["msgCtrl"],
+ ws: {
+ send: vi.fn(),
+ getState: vi.fn(() => "connected"),
+ } as unknown as ChannelControllerOptions["ws"],
+ api: {
+ uploadFile: vi.fn().mockResolvedValue({ id: 1, url: "/f/1", filename: "f.txt" }),
+ } as unknown as ChannelControllerOptions["api"],
+ msgCtrl: {
+ loadMessages: vi.fn(),
+ loadOlderMessages: vi.fn(),
+ } as unknown as ChannelControllerOptions["msgCtrl"],
pendingDeleteManager: { tryDelete: vi.fn(() => "pending" as const), cleanup: vi.fn() },
- reactionCtrl: { handleReaction: vi.fn(), destroy: vi.fn() } as unknown as ChannelControllerOptions["reactionCtrl"],
+ reactionCtrl: {
+ handleReaction: vi.fn(),
+ destroy: vi.fn(),
+ } as unknown as ChannelControllerOptions["reactionCtrl"],
typingLimiter: { tryConsume: vi.fn(() => true) },
showToast: vi.fn(),
getCurrentUserId: () => 1,
@@ -242,7 +276,9 @@ describe("createChannelController", () => {
describe("MessageList callbacks", () => {
it("onDeleteClick sends delete on confirmed", () => {
const opts = makeOpts();
- (opts.pendingDeleteManager.tryDelete as ReturnType).mockReturnValue("confirmed");
+ (opts.pendingDeleteManager.tryDelete as ReturnType).mockReturnValue(
+ "confirmed",
+ );
const ctrl = createChannelController(opts);
ctrl.mountChannel(42, "general");
@@ -372,11 +408,15 @@ describe("createChannelController", () => {
it("onUploadFile shows toast on failure", async () => {
const opts = makeOpts();
- (opts.api as unknown as { uploadFile: ReturnType }).uploadFile.mockRejectedValue(new Error("upload failed"));
+ (
+ opts.api as unknown as { uploadFile: ReturnType }
+ ).uploadFile.mockRejectedValue(new Error("upload failed"));
const ctrl = createChannelController(opts);
ctrl.mountChannel(42, "general");
- await expect(capturedMessageInputOpts!.onUploadFile(new File(["x"], "test.txt"))).rejects.toThrow("upload failed");
+ await expect(
+ capturedMessageInputOpts!.onUploadFile(new File(["x"], "test.txt")),
+ ).rejects.toThrow("upload failed");
expect(opts.showToast).toHaveBeenCalledWith("File upload failed", "error");
});
});
@@ -442,7 +482,9 @@ describe("createChannelController", () => {
it("onPinClick pins a message and shows toast", async () => {
const opts = makeOpts();
- (opts.api as unknown as { pinMessage: ReturnType }).pinMessage = vi.fn().mockResolvedValue(undefined);
+ (opts.api as unknown as { pinMessage: ReturnType }).pinMessage = vi
+ .fn()
+ .mockResolvedValue(undefined);
const ctrl = createChannelController(opts);
ctrl.mountChannel(42, "general");
@@ -456,7 +498,9 @@ describe("createChannelController", () => {
it("onPinClick unpins a message and shows toast", async () => {
const opts = makeOpts();
- (opts.api as unknown as { unpinMessage: ReturnType }).unpinMessage = vi.fn().mockResolvedValue(undefined);
+ (opts.api as unknown as { unpinMessage: ReturnType }).unpinMessage = vi
+ .fn()
+ .mockResolvedValue(undefined);
const ctrl = createChannelController(opts);
ctrl.mountChannel(42, "general");
@@ -470,7 +514,9 @@ describe("createChannelController", () => {
it("onPinClick shows error toast on failure", async () => {
const opts = makeOpts();
- (opts.api as unknown as { pinMessage: ReturnType }).pinMessage = vi.fn().mockRejectedValue(new Error("network error"));
+ (opts.api as unknown as { pinMessage: ReturnType }).pinMessage = vi
+ .fn()
+ .mockRejectedValue(new Error("network error"));
const ctrl = createChannelController(opts);
ctrl.mountChannel(42, "general");
diff --git a/Client/tauri-client/tests/unit/channel-sidebar.test.ts b/Client/tauri-client/tests/unit/channel-sidebar.test.ts
index 2cad8a9d..ef66febb 100644
--- a/Client/tauri-client/tests/unit/channel-sidebar.test.ts
+++ b/Client/tauri-client/tests/unit/channel-sidebar.test.ts
@@ -16,11 +16,7 @@ vi.mock("@lib/streamPreview", () => ({
}));
import { createChannelSidebar } from "../../src/components/ChannelSidebar";
-import {
- channelsStore,
- setChannels,
- setActiveChannel,
-} from "../../src/stores/channels.store";
+import { channelsStore, setChannels, setActiveChannel } from "../../src/stores/channels.store";
import { authStore } from "../../src/stores/auth.store";
import { uiStore, toggleCategory } from "../../src/stores/ui.store";
import { voiceStore, updateVoiceState } from "../../src/stores/voice.store";
@@ -148,9 +144,7 @@ describe("ChannelSidebar", () => {
const items = container.querySelectorAll(".channel-item");
expect(items.length).toBe(4);
- const names = Array.from(
- container.querySelectorAll(".ch-name"),
- ).map((el) => el.textContent);
+ const names = Array.from(container.querySelectorAll(".ch-name")).map((el) => el.textContent);
expect(names).toContain("general");
expect(names).toContain("random");
expect(names).toContain("voice-lobby");
@@ -179,9 +173,7 @@ describe("ChannelSidebar", () => {
const ch1Before = channelsStore.getState().channels.get(1);
expect(ch1Before?.unreadCount).toBe(2);
- const firstItem = container.querySelector(
- '[data-channel-id="1"]',
- ) as HTMLElement;
+ const firstItem = container.querySelector('[data-channel-id="1"]') as HTMLElement;
expect(firstItem).not.toBeNull();
firstItem.click();
@@ -195,9 +187,7 @@ describe("ChannelSidebar", () => {
sidebar.mount(container);
// Text Channels category should have 2 channels visible
- const textChannelsBefore = container.querySelectorAll(
- '.channel-item',
- );
+ const textChannelsBefore = container.querySelectorAll(".channel-item");
expect(textChannelsBefore.length).toBe(4);
// Click the "Text Channels" category header to collapse
@@ -251,9 +241,7 @@ describe("ChannelSidebar", () => {
setActiveChannel(2);
sidebar.mount(container);
- const activeItem = container.querySelector(
- '[data-channel-id="2"]',
- );
+ const activeItem = container.querySelector('[data-channel-id="2"]');
expect(activeItem?.classList.contains("active")).toBe(true);
});
@@ -261,9 +249,7 @@ describe("ChannelSidebar", () => {
setChannels(testChannels);
sidebar.mount(container);
- const voiceItem = container.querySelector(
- '[data-channel-id="3"]',
- );
+ const voiceItem = container.querySelector('[data-channel-id="3"]');
const icon = voiceItem?.querySelector(".ch-icon");
expect(icon).not.toBeNull();
});
@@ -272,9 +258,7 @@ describe("ChannelSidebar", () => {
setChannels(testChannels);
sidebar.mount(container);
- const voiceItem = container.querySelector(
- '[data-channel-id="3"]',
- ) as HTMLElement;
+ const voiceItem = container.querySelector('[data-channel-id="3"]') as HTMLElement;
voiceItem.click();
// Should NOT set active channel
@@ -287,9 +271,7 @@ describe("ChannelSidebar", () => {
setChannels(testChannels);
sidebar.mount(container);
- const textItem = container.querySelector(
- '[data-channel-id="1"]',
- ) as HTMLElement;
+ const textItem = container.querySelector('[data-channel-id="1"]') as HTMLElement;
textItem.click();
expect(channelsStore.getState().activeChannelId).toBe(1);
@@ -301,9 +283,7 @@ describe("ChannelSidebar", () => {
voiceStore.setState((prev) => ({ ...prev, currentChannelId: 3 }));
sidebar.mount(container);
- const voiceItem = container.querySelector(
- '[data-channel-id="3"]',
- ) as HTMLElement;
+ const voiceItem = container.querySelector('[data-channel-id="3"]') as HTMLElement;
voiceItem.click();
expect(onVoiceLeave).toHaveBeenCalled();
@@ -315,7 +295,12 @@ describe("ChannelSidebar", () => {
// Add a member so username resolves
membersStore.setState((prev) => ({
...prev,
- members: new Map([[10, { id: 10, username: "Alice", avatar: null, role: "member", status: "online" as const }]]),
+ members: new Map([
+ [
+ 10,
+ { id: 10, username: "Alice", avatar: null, role: "member", status: "online" as const },
+ ],
+ ]),
}));
updateVoiceState({
channel_id: 3,
@@ -344,9 +329,7 @@ describe("ChannelSidebar", () => {
voiceStore.setState((prev) => ({ ...prev, currentChannelId: 3 }));
sidebar.mount(container);
- const voiceItem = container.querySelector(
- '[data-channel-id="3"]',
- );
+ const voiceItem = container.querySelector('[data-channel-id="3"]');
expect(voiceItem?.classList.contains("active")).toBe(true);
});
@@ -640,11 +623,13 @@ describe("ChannelSidebar", () => {
expect(channelEl).not.toBeNull();
// Dispatch right-click
- channelEl.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 100,
- clientY: 200,
- }));
+ channelEl.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 100,
+ clientY: 200,
+ }),
+ );
const ctxMenu = document.querySelector('[data-testid="channel-context-menu"]');
expect(ctxMenu).not.toBeNull();
@@ -672,11 +657,13 @@ describe("ChannelSidebar", () => {
sidebar.mount(container);
const channelEl = container.querySelector('[data-channel-id="1"]') as HTMLElement;
- channelEl.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 100,
- clientY: 200,
- }));
+ channelEl.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 100,
+ clientY: 200,
+ }),
+ );
const editItem = document.querySelector('[data-testid="ctx-edit-channel"]') as HTMLElement;
editItem.click();
@@ -701,11 +688,13 @@ describe("ChannelSidebar", () => {
sidebar.mount(container);
const channelEl = container.querySelector('[data-channel-id="1"]') as HTMLElement;
- channelEl.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 100,
- clientY: 200,
- }));
+ channelEl.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 100,
+ clientY: 200,
+ }),
+ );
const deleteItem = document.querySelector('[data-testid="ctx-delete-channel"]') as HTMLElement;
deleteItem.click();
@@ -735,11 +724,13 @@ describe("ChannelSidebar", () => {
sidebar.mount(container);
const channelEl = container.querySelector('[data-channel-id="1"]') as HTMLElement;
- channelEl.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 100,
- clientY: 200,
- }));
+ channelEl.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 100,
+ clientY: 200,
+ }),
+ );
// No context menu should appear for non-admin
const ctxMenu = document.querySelector('[data-testid="channel-context-menu"]');
@@ -779,7 +770,9 @@ describe("ChannelSidebar", () => {
setChannels(testChannels);
sidebar.mount(container);
- const addBtn = container.querySelector('[data-testid="create-channel-text-channels"]') as HTMLElement;
+ const addBtn = container.querySelector(
+ '[data-testid="create-channel-text-channels"]',
+ ) as HTMLElement;
addBtn.click();
expect(onCreateChannel).toHaveBeenCalledWith("Text Channels");
@@ -801,7 +794,9 @@ describe("ChannelSidebar", () => {
// All 4 channels visible before click
expect(container.querySelectorAll(".channel-item").length).toBe(4);
- const addBtn = container.querySelector('[data-testid="create-channel-text-channels"]') as HTMLElement;
+ const addBtn = container.querySelector(
+ '[data-testid="create-channel-text-channels"]',
+ ) as HTMLElement;
addBtn.click();
// Category should NOT have collapsed (stopPropagation in the handler)
@@ -857,11 +852,13 @@ describe("ChannelSidebar", () => {
sidebar.mount(container);
const voiceRow = container.querySelector(".voice-user-item") as HTMLElement;
- voiceRow.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 150,
- clientY: 250,
- }));
+ voiceRow.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 150,
+ clientY: 250,
+ }),
+ );
const volMenu = document.querySelector(".user-vol-menu");
expect(volMenu).not.toBeNull();
@@ -1011,9 +1008,7 @@ describe("ChannelSidebar", () => {
const userItems = container.querySelectorAll(".voice-user-item");
expect(userItems.length).toBe(2);
- const names = Array.from(userItems).map(
- (el) => el.querySelector(".vu-name")?.textContent,
- );
+ const names = Array.from(userItems).map((el) => el.querySelector(".vu-name")?.textContent);
expect(names).toContain("UserA");
expect(names).toContain("UserB");
});
@@ -1052,7 +1047,23 @@ describe("ChannelSidebar", () => {
voiceStore.setState(() => ({
currentChannelId: 3,
voiceUsers: new Map([
- [3, new Map([[99, { userId: 99, username: "Streamer", speaking: false, muted: false, deafened: false, camera: false, screenshare: true }]])],
+ [
+ 3,
+ new Map([
+ [
+ 99,
+ {
+ userId: 99,
+ username: "Streamer",
+ speaking: false,
+ muted: false,
+ deafened: false,
+ camera: false,
+ screenshare: true,
+ },
+ ],
+ ]),
+ ],
]),
voiceConfigs: new Map(),
localMuted: false,
@@ -1080,7 +1091,23 @@ describe("ChannelSidebar", () => {
voiceStore.setState(() => ({
currentChannelId: 3,
voiceUsers: new Map([
- [3, new Map([[99, { userId: 99, username: "Cammer", speaking: false, muted: false, deafened: false, camera: true, screenshare: false }]])],
+ [
+ 3,
+ new Map([
+ [
+ 99,
+ {
+ userId: 99,
+ username: "Cammer",
+ speaking: false,
+ muted: false,
+ deafened: false,
+ camera: true,
+ screenshare: false,
+ },
+ ],
+ ]),
+ ],
]),
voiceConfigs: new Map(),
localMuted: false,
@@ -1114,7 +1141,23 @@ describe("ChannelSidebar", () => {
voiceStore.setState(() => ({
currentChannelId: 3,
voiceUsers: new Map([
- [3, new Map([[42, { userId: 42, username: "Me", speaking: false, muted: false, deafened: false, camera: true, screenshare: false }]])],
+ [
+ 3,
+ new Map([
+ [
+ 42,
+ {
+ userId: 42,
+ username: "Me",
+ speaking: false,
+ muted: false,
+ deafened: false,
+ camera: true,
+ screenshare: false,
+ },
+ ],
+ ]),
+ ],
]),
voiceConfigs: new Map(),
localMuted: false,
@@ -1139,7 +1182,23 @@ describe("ChannelSidebar", () => {
voiceStore.setState(() => ({
currentChannelId: 3,
voiceUsers: new Map([
- [3, new Map([[1, { userId: 1, username: "User", speaking: false, muted: false, deafened: false, camera: false, screenshare: true }]])],
+ [
+ 3,
+ new Map([
+ [
+ 1,
+ {
+ userId: 1,
+ username: "User",
+ speaking: false,
+ muted: false,
+ deafened: false,
+ camera: false,
+ screenshare: true,
+ },
+ ],
+ ]),
+ ],
]),
voiceConfigs: new Map(),
localMuted: false,
@@ -1164,7 +1223,23 @@ describe("ChannelSidebar", () => {
voiceStore.setState(() => ({
currentChannelId: 3,
voiceUsers: new Map([
- [3, new Map([[99, { userId: 99, username: "User", speaking: false, muted: false, deafened: false, camera: true, screenshare: false }]])],
+ [
+ 3,
+ new Map([
+ [
+ 99,
+ {
+ userId: 99,
+ username: "User",
+ speaking: false,
+ muted: false,
+ deafened: false,
+ camera: true,
+ screenshare: false,
+ },
+ ],
+ ]),
+ ],
]),
voiceConfigs: new Map(),
localMuted: false,
diff --git a/Client/tauri-client/tests/unit/channels.store.test.ts b/Client/tauri-client/tests/unit/channels.store.test.ts
index fab1a104..7614ffcf 100644
--- a/Client/tauri-client/tests/unit/channels.store.test.ts
+++ b/Client/tauri-client/tests/unit/channels.store.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect, beforeEach } from 'vitest';
+import { describe, it, expect, beforeEach } from "vitest";
import {
channelsStore,
setChannels,
@@ -13,12 +13,8 @@ import {
getChannelsByCategory,
incrementUnread,
clearUnread,
-} from '../../src/stores/channels.store';
-import type {
- ReadyChannel,
- ChannelCreatePayload,
- ChannelUpdatePayload,
-} from '../../src/lib/types';
+} from "../../src/stores/channels.store";
+import type { ReadyChannel, ChannelCreatePayload, ChannelUpdatePayload } from "../../src/lib/types";
function resetStore(): void {
channelsStore.setState(() => ({
@@ -29,24 +25,40 @@ function resetStore(): void {
}
const readyChannels: ReadyChannel[] = [
- { id: 1, name: 'general', type: 'text', category: 'Text', position: 0, unread_count: 3, last_message_id: 100 },
- { id: 2, name: 'voice-lobby', type: 'voice', category: 'Voice', position: 0 },
- { id: 3, name: 'announcements', type: 'announcement', category: 'Text', position: 1, unread_count: 0, last_message_id: 50 },
+ {
+ id: 1,
+ name: "general",
+ type: "text",
+ category: "Text",
+ position: 0,
+ unread_count: 3,
+ last_message_id: 100,
+ },
+ { id: 2, name: "voice-lobby", type: "voice", category: "Voice", position: 0 },
+ {
+ id: 3,
+ name: "announcements",
+ type: "announcement",
+ category: "Text",
+ position: 1,
+ unread_count: 0,
+ last_message_id: 50,
+ },
];
-describe('channels store', () => {
+describe("channels store", () => {
beforeEach(() => {
resetStore();
});
- it('has empty initial state', () => {
+ it("has empty initial state", () => {
const state = channelsStore.getState();
expect(state.channels.size).toBe(0);
expect(state.activeChannelId).toBeNull();
});
- describe('setChannels', () => {
- it('populates channels from ready payload', () => {
+ describe("setChannels", () => {
+ it("populates channels from ready payload", () => {
setChannels(readyChannels);
const state = channelsStore.getState();
@@ -55,9 +67,9 @@ describe('channels store', () => {
const general = state.channels.get(1);
expect(general).toEqual({
id: 1,
- name: 'general',
- type: 'text',
- category: 'Text',
+ name: "general",
+ type: "text",
+ category: "Text",
position: 0,
unreadCount: 3,
lastMessageId: 100,
@@ -66,32 +78,32 @@ describe('channels store', () => {
const voice = state.channels.get(2);
expect(voice).toEqual({
id: 2,
- name: 'voice-lobby',
- type: 'voice',
- category: 'Voice',
+ name: "voice-lobby",
+ type: "voice",
+ category: "Voice",
position: 0,
unreadCount: 0,
lastMessageId: null,
});
});
- it('defaults unread_count to 0 and last_message_id to null', () => {
- setChannels([{ id: 10, name: 'test', type: 'text', category: null, position: 0 }]);
+ it("defaults unread_count to 0 and last_message_id to null", () => {
+ setChannels([{ id: 10, name: "test", type: "text", category: null, position: 0 }]);
const ch = channelsStore.getState().channels.get(10);
expect(ch?.unreadCount).toBe(0);
expect(ch?.lastMessageId).toBeNull();
});
});
- describe('addChannel', () => {
- it('adds a new channel', () => {
+ describe("addChannel", () => {
+ it("adds a new channel", () => {
setChannels(readyChannels);
const payload: ChannelCreatePayload = {
id: 4,
- name: 'new-channel',
- type: 'text',
- category: 'Text',
+ name: "new-channel",
+ type: "text",
+ category: "Text",
position: 2,
};
addChannel(payload);
@@ -102,20 +114,20 @@ describe('channels store', () => {
const added = state.channels.get(4);
expect(added).toEqual({
id: 4,
- name: 'new-channel',
- type: 'text',
- category: 'Text',
+ name: "new-channel",
+ type: "text",
+ category: "Text",
position: 2,
unreadCount: 0,
lastMessageId: null,
});
});
- it('does not mutate the previous channels map', () => {
+ it("does not mutate the previous channels map", () => {
setChannels(readyChannels);
const before = channelsStore.getState().channels;
- addChannel({ id: 5, name: 'extra', type: 'text', category: null, position: 0 });
+ addChannel({ id: 5, name: "extra", type: "text", category: null, position: 0 });
const after = channelsStore.getState().channels;
expect(before).not.toBe(after);
@@ -124,53 +136,53 @@ describe('channels store', () => {
});
});
- describe('updateChannel', () => {
- it('updates name immutably', () => {
+ describe("updateChannel", () => {
+ it("updates name immutably", () => {
setChannels(readyChannels);
const before = channelsStore.getState().channels.get(1);
- const update: ChannelUpdatePayload = { id: 1, name: 'renamed' };
+ const update: ChannelUpdatePayload = { id: 1, name: "renamed" };
updateChannel(update);
const after = channelsStore.getState().channels.get(1);
- expect(after?.name).toBe('renamed');
+ expect(after?.name).toBe("renamed");
expect(after?.position).toBe(0); // unchanged
expect(before).not.toBe(after);
});
- it('updates position immutably', () => {
+ it("updates position immutably", () => {
setChannels(readyChannels);
updateChannel({ id: 1, position: 5 });
const ch = channelsStore.getState().channels.get(1);
expect(ch?.position).toBe(5);
- expect(ch?.name).toBe('general'); // unchanged
+ expect(ch?.name).toBe("general"); // unchanged
});
- it('updates both name and position', () => {
+ it("updates both name and position", () => {
setChannels(readyChannels);
- updateChannel({ id: 1, name: 'new-name', position: 10 });
+ updateChannel({ id: 1, name: "new-name", position: 10 });
const ch = channelsStore.getState().channels.get(1);
- expect(ch?.name).toBe('new-name');
+ expect(ch?.name).toBe("new-name");
expect(ch?.position).toBe(10);
});
- it('is a no-op for unknown channel id', () => {
+ it("is a no-op for unknown channel id", () => {
setChannels(readyChannels);
const before = channelsStore.getState();
- updateChannel({ id: 999, name: 'ghost' });
+ updateChannel({ id: 999, name: "ghost" });
const after = channelsStore.getState();
expect(after).toBe(before);
});
});
- describe('removeChannel', () => {
- it('removes a channel', () => {
+ describe("removeChannel", () => {
+ it("removes a channel", () => {
setChannels(readyChannels);
removeChannel(1);
@@ -180,7 +192,7 @@ describe('channels store', () => {
expect(state.channels.has(1)).toBe(false);
});
- it('clears activeChannelId if removed channel was active', () => {
+ it("clears activeChannelId if removed channel was active", () => {
setChannels(readyChannels);
setActiveChannel(1);
expect(channelsStore.getState().activeChannelId).toBe(1);
@@ -190,7 +202,7 @@ describe('channels store', () => {
expect(channelsStore.getState().activeChannelId).toBeNull();
});
- it('preserves activeChannelId if removed channel was not active', () => {
+ it("preserves activeChannelId if removed channel was not active", () => {
setChannels(readyChannels);
setActiveChannel(2);
@@ -200,8 +212,8 @@ describe('channels store', () => {
});
});
- describe('setActiveChannel', () => {
- it('sets active channel id', () => {
+ describe("setActiveChannel", () => {
+ it("sets active channel id", () => {
setChannels(readyChannels);
setActiveChannel(2);
@@ -209,7 +221,7 @@ describe('channels store', () => {
expect(channelsStore.getState().activeChannelId).toBe(2);
});
- it('sets active channel to null', () => {
+ it("sets active channel to null", () => {
setChannels(readyChannels);
setActiveChannel(1);
@@ -218,7 +230,7 @@ describe('channels store', () => {
expect(channelsStore.getState().activeChannelId).toBeNull();
});
- it('clears unread count for the activated channel', () => {
+ it("clears unread count for the activated channel", () => {
setChannels(readyChannels);
// channel 1 starts with unreadCount: 3
expect(channelsStore.getState().channels.get(1)?.unreadCount).toBe(3);
@@ -228,7 +240,7 @@ describe('channels store', () => {
expect(channelsStore.getState().channels.get(1)?.unreadCount).toBe(0);
});
- it('does not mutate channels map when clearing unread', () => {
+ it("does not mutate channels map when clearing unread", () => {
setChannels(readyChannels);
const before = channelsStore.getState().channels;
@@ -240,7 +252,7 @@ describe('channels store', () => {
expect(after.get(2)).toBe(before.get(2));
});
- it('skips channels map update when unread is already 0', () => {
+ it("skips channels map update when unread is already 0", () => {
setChannels(readyChannels);
// channel 2 has unreadCount: 0
const before = channelsStore.getState().channels;
@@ -252,70 +264,68 @@ describe('channels store', () => {
});
});
- describe('getActiveChannel', () => {
- it('returns null when no active channel', () => {
+ describe("getActiveChannel", () => {
+ it("returns null when no active channel", () => {
expect(getActiveChannel()).toBeNull();
});
- it('returns the active Channel object', () => {
+ it("returns the active Channel object", () => {
setChannels(readyChannels);
setActiveChannel(1);
const active = getActiveChannel();
expect(active).toEqual({
id: 1,
- name: 'general',
- type: 'text',
- category: 'Text',
+ name: "general",
+ type: "text",
+ category: "Text",
position: 0,
unreadCount: 0,
lastMessageId: 100,
});
});
- it('returns null if activeChannelId refers to a non-existent channel', () => {
+ it("returns null if activeChannelId refers to a non-existent channel", () => {
setActiveChannel(999);
expect(getActiveChannel()).toBeNull();
});
});
- describe('getChannelsByCategory', () => {
- it('groups channels by category and sorts by position', () => {
+ describe("getChannelsByCategory", () => {
+ it("groups channels by category and sorts by position", () => {
setChannels(readyChannels);
const grouped = getChannelsByCategory();
expect(grouped.size).toBe(2);
- const textChannels = grouped.get('Text');
+ const textChannels = grouped.get("Text");
expect(textChannels).toHaveLength(2);
- expect(textChannels?.[0]?.name).toBe('general'); // position 0
- expect(textChannels?.[1]?.name).toBe('announcements'); // position 1
+ expect(textChannels?.[0]?.name).toBe("general"); // position 0
+ expect(textChannels?.[1]?.name).toBe("announcements"); // position 1
- const voiceChannels = grouped.get('Voice');
+ const voiceChannels = grouped.get("Voice");
expect(voiceChannels).toHaveLength(1);
- expect(voiceChannels?.[0]?.name).toBe('voice-lobby');
+ expect(voiceChannels?.[0]?.name).toBe("voice-lobby");
});
- it('handles null category', () => {
- setChannels([
- { id: 1, name: 'uncategorized', type: 'text', category: null, position: 0 },
- ]);
+ it("handles null category", () => {
+ setChannels([{ id: 1, name: "uncategorized", type: "text", category: null, position: 0 }]);
const grouped = getChannelsByCategory();
expect(grouped.has(null)).toBe(true);
expect(grouped.get(null)).toHaveLength(1);
});
- it('returns empty map when no channels', () => {
+ it("returns empty map when no channels", () => {
const grouped = getChannelsByCategory();
expect(grouped.size).toBe(0);
});
});
- describe('incrementUnread', () => {
- it('increments unread count for a channel', () => {
+ describe("incrementUnread", () => {
+ it("increments unread count for a channel", () => {
setChannels(readyChannels);
incrementUnread(1);
@@ -324,7 +334,7 @@ describe('channels store', () => {
expect(ch?.unreadCount).toBe(4); // was 3
});
- it('skips increment for the active channel', () => {
+ it("skips increment for the active channel", () => {
setChannels(readyChannels);
setActiveChannel(1);
// setActiveChannel clears unread, so it's now 0
@@ -336,7 +346,7 @@ describe('channels store', () => {
expect(ch?.unreadCount).toBe(0); // unchanged — active channel skips increment
});
- it('is a no-op for unknown channel id', () => {
+ it("is a no-op for unknown channel id", () => {
setChannels(readyChannels);
const before = channelsStore.getState();
@@ -346,8 +356,8 @@ describe('channels store', () => {
});
});
- describe('clearUnread', () => {
- it('resets unread count to 0', () => {
+ describe("clearUnread", () => {
+ it("resets unread count to 0", () => {
setChannels(readyChannels);
expect(channelsStore.getState().channels.get(1)?.unreadCount).toBe(3);
@@ -356,7 +366,7 @@ describe('channels store', () => {
expect(channelsStore.getState().channels.get(1)?.unreadCount).toBe(0);
});
- it('is a no-op for unknown channel id', () => {
+ it("is a no-op for unknown channel id", () => {
setChannels(readyChannels);
const before = channelsStore.getState();
@@ -366,47 +376,47 @@ describe('channels store', () => {
});
});
- describe('setRoles', () => {
- it('stores roles from ready payload', () => {
+ describe("setRoles", () => {
+ it("stores roles from ready payload", () => {
const roles = [
- { id: 1, name: 'admin', color: '#ff0000', permissions: 0 },
- { id: 2, name: 'member', color: '#00ff00', permissions: 0 },
+ { id: 1, name: "admin", color: "#ff0000", permissions: 0 },
+ { id: 2, name: "member", color: "#00ff00", permissions: 0 },
];
setRoles(roles);
expect(channelsStore.getState().roles).toEqual(roles);
});
- it('replaces existing roles', () => {
- setRoles([{ id: 1, name: 'admin', color: '#ff0000', permissions: 0 }]);
- setRoles([{ id: 2, name: 'member', color: '#00ff00', permissions: 0 }]);
+ it("replaces existing roles", () => {
+ setRoles([{ id: 1, name: "admin", color: "#ff0000", permissions: 0 }]);
+ setRoles([{ id: 2, name: "member", color: "#00ff00", permissions: 0 }]);
expect(channelsStore.getState().roles).toHaveLength(1);
- expect(channelsStore.getState().roles[0]!.name).toBe('member');
+ expect(channelsStore.getState().roles[0]!.name).toBe("member");
});
});
- describe('getRoleIdByName', () => {
- it('returns role id for matching name (case-insensitive)', () => {
+ describe("getRoleIdByName", () => {
+ it("returns role id for matching name (case-insensitive)", () => {
setRoles([
- { id: 1, name: 'Admin', color: '#ff0000', permissions: 0 },
- { id: 2, name: 'Member', color: '#00ff00', permissions: 0 },
+ { id: 1, name: "Admin", color: "#ff0000", permissions: 0 },
+ { id: 2, name: "Member", color: "#00ff00", permissions: 0 },
]);
- expect(getRoleIdByName('admin')).toBe(1);
- expect(getRoleIdByName('ADMIN')).toBe(1);
- expect(getRoleIdByName('member')).toBe(2);
+ expect(getRoleIdByName("admin")).toBe(1);
+ expect(getRoleIdByName("ADMIN")).toBe(1);
+ expect(getRoleIdByName("member")).toBe(2);
});
- it('returns undefined for non-existent role', () => {
- setRoles([{ id: 1, name: 'admin', color: '#ff0000', permissions: 0 }]);
- expect(getRoleIdByName('moderator')).toBeUndefined();
+ it("returns undefined for non-existent role", () => {
+ setRoles([{ id: 1, name: "admin", color: "#ff0000", permissions: 0 }]);
+ expect(getRoleIdByName("moderator")).toBeUndefined();
});
- it('returns undefined when no roles set', () => {
- expect(getRoleIdByName('admin')).toBeUndefined();
+ it("returns undefined when no roles set", () => {
+ expect(getRoleIdByName("admin")).toBeUndefined();
});
});
- describe('updateChannelPosition', () => {
- it('updates a channel position', () => {
+ describe("updateChannelPosition", () => {
+ it("updates a channel position", () => {
setChannels(readyChannels);
updateChannelPosition(1, 5);
@@ -414,7 +424,7 @@ describe('channels store', () => {
expect(channelsStore.getState().channels.get(1)?.position).toBe(5);
});
- it('is a no-op for unknown channel id', () => {
+ it("is a no-op for unknown channel id", () => {
setChannels(readyChannels);
const before = channelsStore.getState();
@@ -423,7 +433,7 @@ describe('channels store', () => {
expect(channelsStore.getState()).toBe(before);
});
- it('is a no-op when position is already the same', () => {
+ it("is a no-op when position is already the same", () => {
setChannels(readyChannels);
const before = channelsStore.getState();
@@ -432,7 +442,7 @@ describe('channels store', () => {
expect(channelsStore.getState()).toBe(before);
});
- it('produces a new channel object (immutable)', () => {
+ it("produces a new channel object (immutable)", () => {
setChannels(readyChannels);
const before = channelsStore.getState().channels.get(1);
@@ -444,56 +454,56 @@ describe('channels store', () => {
});
});
- describe('getChannelsByCategory — DM filtering', () => {
- it('excludes DM channels from category grouping', () => {
+ describe("getChannelsByCategory — DM filtering", () => {
+ it("excludes DM channels from category grouping", () => {
setChannels([
- { id: 1, name: 'general', type: 'text', category: 'Text', position: 0 },
- { id: 2, name: 'dm-channel', type: 'dm', category: null, position: 0 },
+ { id: 1, name: "general", type: "text", category: "Text", position: 0 },
+ { id: 2, name: "dm-channel", type: "dm", category: null, position: 0 },
]);
const grouped = getChannelsByCategory();
// DM channels should be filtered out
expect(grouped.size).toBe(1);
- expect(grouped.has('Text')).toBe(true);
+ expect(grouped.has("Text")).toBe(true);
// Verify the DM is not in any group
for (const channels of grouped.values()) {
- expect(channels.every((ch) => ch.type !== 'dm')).toBe(true);
+ expect(channels.every((ch) => ch.type !== "dm")).toBe(true);
}
});
});
- describe('getChannelsByCategory — sort order', () => {
- it('sorts channels within same category by position', () => {
+ describe("getChannelsByCategory — sort order", () => {
+ it("sorts channels within same category by position", () => {
setChannels([
- { id: 1, name: 'c-channel', type: 'text', category: 'Text', position: 2 },
- { id: 2, name: 'a-channel', type: 'text', category: 'Text', position: 0 },
- { id: 3, name: 'b-channel', type: 'text', category: 'Text', position: 1 },
+ { id: 1, name: "c-channel", type: "text", category: "Text", position: 2 },
+ { id: 2, name: "a-channel", type: "text", category: "Text", position: 0 },
+ { id: 3, name: "b-channel", type: "text", category: "Text", position: 1 },
]);
const grouped = getChannelsByCategory();
- const textChannels = grouped.get('Text')!;
- expect(textChannels[0]!.name).toBe('a-channel');
- expect(textChannels[1]!.name).toBe('b-channel');
- expect(textChannels[2]!.name).toBe('c-channel');
+ const textChannels = grouped.get("Text")!;
+ expect(textChannels[0]!.name).toBe("a-channel");
+ expect(textChannels[1]!.name).toBe("b-channel");
+ expect(textChannels[2]!.name).toBe("c-channel");
});
});
- describe('setActiveChannel — edge case: unknown channel with unread=0', () => {
- it('sets activeChannelId even for a channel not in the map', () => {
+ describe("setActiveChannel — edge case: unknown channel with unread=0", () => {
+ it("sets activeChannelId even for a channel not in the map", () => {
setActiveChannel(999);
expect(channelsStore.getState().activeChannelId).toBe(999);
});
});
- describe('updateChannel — no changes', () => {
- it('still creates new object when neither name nor position is provided', () => {
+ describe("updateChannel — no changes", () => {
+ it("still creates new object when neither name nor position is provided", () => {
setChannels(readyChannels);
updateChannel({ id: 1 } as ChannelUpdatePayload);
// Channel should still exist with original values
const ch = channelsStore.getState().channels.get(1);
- expect(ch?.name).toBe('general');
+ expect(ch?.name).toBe("general");
expect(ch?.position).toBe(0);
});
});
diff --git a/Client/tauri-client/tests/unit/chat-area.test.ts b/Client/tauri-client/tests/unit/chat-area.test.ts
index 4fafc1b6..83d62c57 100644
--- a/Client/tauri-client/tests/unit/chat-area.test.ts
+++ b/Client/tauri-client/tests/unit/chat-area.test.ts
@@ -26,7 +26,10 @@ vi.mock("@lib/icons", () => ({
vi.mock("@lib/logger", () => ({
createLogger: () => ({
- debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(),
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
}),
}));
@@ -65,7 +68,10 @@ vi.mock("@components/VideoGrid", () => ({
import { createChatArea } from "../../src/pages/main-page/ChatArea";
import type { ChatAreaOptions } from "../../src/pages/main-page/ChatArea";
-import { createPinnedPanelController, createSearchOverlayController } from "../../src/pages/main-page/OverlayManagers";
+import {
+ createPinnedPanelController,
+ createSearchOverlayController,
+} from "../../src/pages/main-page/OverlayManagers";
// ---------------------------------------------------------------------------
// Helpers
@@ -73,7 +79,11 @@ import { createPinnedPanelController, createSearchOverlayController } from "../.
function makeOptions(overrides: Partial = {}): ChatAreaOptions {
return {
- api: { getPins: vi.fn(), search: vi.fn(), unpinMessage: vi.fn() } as unknown as ChatAreaOptions["api"],
+ api: {
+ getPins: vi.fn(),
+ search: vi.fn(),
+ unpinMessage: vi.fn(),
+ } as unknown as ChatAreaOptions["api"],
getRoot: () => document.createElement("div"),
getToast: () => null,
getChannelCtrl: () => null,
@@ -306,7 +316,9 @@ describe("createChatArea", () => {
it("focusing search input opens the search overlay controller", () => {
const result = createChatArea(makeOptions());
- const searchInput = result.chatArea.querySelector("[data-testid='search-input']") as HTMLInputElement;
+ const searchInput = result.chatArea.querySelector(
+ "[data-testid='search-input']",
+ ) as HTMLInputElement;
expect(searchInput).not.toBeNull();
searchInput.dispatchEvent(new Event("focus"));
expect(mockSearchOpen).toHaveBeenCalledTimes(1);
diff --git a/Client/tauri-client/tests/unit/chat-header.test.ts b/Client/tauri-client/tests/unit/chat-header.test.ts
index 51c6048f..62754fcc 100644
--- a/Client/tauri-client/tests/unit/chat-header.test.ts
+++ b/Client/tauri-client/tests/unit/chat-header.test.ts
@@ -29,7 +29,9 @@ describe("ChatHeader", () => {
container.appendChild(element);
expect(refs.nameEl.textContent).toBe("general");
- expect(container.querySelector('[data-testid="chat-header-name"]')?.textContent).toBe("general");
+ expect(container.querySelector('[data-testid="chat-header-name"]')?.textContent).toBe(
+ "general",
+ );
});
it("displays hash prefix", () => {
@@ -73,7 +75,9 @@ describe("ChatHeader", () => {
// Update name via ref
refs.nameEl.textContent = "announcements";
- expect(container.querySelector('[data-testid="chat-header-name"]')?.textContent).toBe("announcements");
+ expect(container.querySelector('[data-testid="chat-header-name"]')?.textContent).toBe(
+ "announcements",
+ );
// Update topic via ref
refs.topicEl.textContent = "Important news";
diff --git a/Client/tauri-client/tests/unit/chat.test.ts b/Client/tauri-client/tests/unit/chat.test.ts
index a8b88b7f..676ca99b 100644
--- a/Client/tauri-client/tests/unit/chat.test.ts
+++ b/Client/tauri-client/tests/unit/chat.test.ts
@@ -9,9 +9,15 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// jsdom does not provide ResizeObserver — stub it so MessageList can mount.
if (typeof globalThis.ResizeObserver === "undefined") {
globalThis.ResizeObserver = class {
- observe(): void { /* noop */ }
- unobserve(): void { /* noop */ }
- disconnect(): void { /* noop */ }
+ observe(): void {
+ /* noop */
+ }
+ unobserve(): void {
+ /* noop */
+ }
+ disconnect(): void {
+ /* noop */
+ }
} as unknown as typeof ResizeObserver;
}
@@ -20,11 +26,7 @@ if (typeof globalThis.ResizeObserver === "undefined") {
// ---------------------------------------------------------------------------
import { createMessageList } from "../../src/components/MessageList";
-import {
- messagesStore,
- addMessage,
- setMessages,
-} from "../../src/stores/messages.store";
+import { messagesStore, addMessage, setMessages } from "../../src/stores/messages.store";
import { membersStore, setMembers } from "../../src/stores/members.store";
// Reset stores before each test
@@ -105,14 +107,14 @@ describe("MessageList", () => {
expect(messagesContainer).not.toBeNull();
const welcome = messagesContainer?.querySelector(".channel-welcome");
expect(welcome).not.toBeNull();
- expect(welcome?.querySelector(".channel-welcome-title")?.textContent).toBe("Welcome to #general!");
+ expect(welcome?.querySelector(".channel-welcome-title")?.textContent).toBe(
+ "Welcome to #general!",
+ );
list.destroy?.();
});
it("renders messages after store update", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "Hello", "2026-03-15T10:00:00Z"),
- ], false);
+ setMessages(1, [makeMessage(1, 10, "Alice", "Hello", "2026-03-15T10:00:00Z")], false);
const list = createMessageList({
channelId: 1,
@@ -136,11 +138,15 @@ describe("MessageList", () => {
describe("message grouping", () => {
it("groups consecutive messages from same user within 5 minutes", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "Hi", "2026-03-15T10:00:00Z"),
- makeMessage(2, 10, "Alice", "How are you?", "2026-03-15T10:02:00Z"),
- makeMessage(3, 10, "Alice", "Anyone there?", "2026-03-15T10:04:00Z"),
- ], false);
+ setMessages(
+ 1,
+ [
+ makeMessage(1, 10, "Alice", "Hi", "2026-03-15T10:00:00Z"),
+ makeMessage(2, 10, "Alice", "How are you?", "2026-03-15T10:02:00Z"),
+ makeMessage(3, 10, "Alice", "Anyone there?", "2026-03-15T10:04:00Z"),
+ ],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -164,10 +170,14 @@ describe("MessageList", () => {
});
it("breaks group when user changes", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "Hi", "2026-03-15T10:00:00Z"),
- makeMessage(2, 20, "Bob", "Hey!", "2026-03-15T10:01:00Z"),
- ], false);
+ setMessages(
+ 1,
+ [
+ makeMessage(1, 10, "Alice", "Hi", "2026-03-15T10:00:00Z"),
+ makeMessage(2, 20, "Bob", "Hey!", "2026-03-15T10:01:00Z"),
+ ],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -188,10 +198,14 @@ describe("MessageList", () => {
});
it("breaks group when gap exceeds 5 minutes", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "Hi", "2026-03-15T10:00:00Z"),
- makeMessage(2, 10, "Alice", "Later", "2026-03-15T10:10:00Z"),
- ], false);
+ setMessages(
+ 1,
+ [
+ makeMessage(1, 10, "Alice", "Hi", "2026-03-15T10:00:00Z"),
+ makeMessage(2, 10, "Alice", "Later", "2026-03-15T10:10:00Z"),
+ ],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -214,10 +228,14 @@ describe("MessageList", () => {
describe("day dividers", () => {
it("inserts day divider between messages on different days", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "Day 1", "2026-03-10T12:00:00Z"),
- makeMessage(2, 10, "Alice", "Day 2", "2026-03-15T12:00:00Z"),
- ], false);
+ setMessages(
+ 1,
+ [
+ makeMessage(1, 10, "Alice", "Day 1", "2026-03-10T12:00:00Z"),
+ makeMessage(2, 10, "Alice", "Day 2", "2026-03-15T12:00:00Z"),
+ ],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -240,9 +258,11 @@ describe("MessageList", () => {
describe("@mention parsing", () => {
it("wraps @username in .mention span", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "Hey @Bob check this", "2026-03-15T10:00:00Z"),
- ], false);
+ setMessages(
+ 1,
+ [makeMessage(1, 10, "Alice", "Hey @Bob check this", "2026-03-15T10:00:00Z")],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -264,9 +284,11 @@ describe("MessageList", () => {
});
it("handles multiple @mentions in one message", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "@Bob and @Charlie look", "2026-03-15T10:00:00Z"),
- ], false);
+ setMessages(
+ 1,
+ [makeMessage(1, 10, "Alice", "@Bob and @Charlie look", "2026-03-15T10:00:00Z")],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -289,9 +311,11 @@ describe("MessageList", () => {
describe("deleted and edited messages", () => {
it("shows [message deleted] for deleted messages", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "secret", "2026-03-15T10:00:00Z", { deleted: true }),
- ], false);
+ setMessages(
+ 1,
+ [makeMessage(1, 10, "Alice", "secret", "2026-03-15T10:00:00Z", { deleted: true })],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -312,11 +336,15 @@ describe("MessageList", () => {
});
it("shows (edited) indicator for edited messages", () => {
- setMessages(1, [
- makeMessage(1, 10, "Alice", "updated text", "2026-03-15T10:00:00Z", {
- editedAt: "2026-03-15T10:05:00Z",
- }),
- ], false);
+ setMessages(
+ 1,
+ [
+ makeMessage(1, 10, "Alice", "updated text", "2026-03-15T10:00:00Z", {
+ editedAt: "2026-03-15T10:05:00Z",
+ }),
+ ],
+ false,
+ );
const list = createMessageList({
channelId: 1,
@@ -339,9 +367,7 @@ describe("MessageList", () => {
describe("system messages", () => {
it("applies msg--system class to System user messages", () => {
- setMessages(1, [
- makeMessage(1, 0, "System", "Alice joined", "2026-03-15T10:00:00Z"),
- ], false);
+ setMessages(1, [makeMessage(1, 0, "System", "Alice joined", "2026-03-15T10:00:00Z")], false);
const list = createMessageList({
channelId: 1,
diff --git a/Client/tauri-client/tests/unit/connect-page.test.ts b/Client/tauri-client/tests/unit/connect-page.test.ts
index 035ba5cc..8353661f 100644
--- a/Client/tauri-client/tests/unit/connect-page.test.ts
+++ b/Client/tauri-client/tests/unit/connect-page.test.ts
@@ -38,9 +38,7 @@ function makeCallbacks(overrides: Partial = {}): ConnectPa
};
}
-const testProfiles: SimpleProfile[] = [
- { name: "Test Server", host: "localhost:8443" },
-];
+const testProfiles: SimpleProfile[] = [{ name: "Test Server", host: "localhost:8443" }];
describe("ConnectPage", () => {
let container: HTMLDivElement;
@@ -228,7 +226,9 @@ describe("ConnectPage", () => {
it("disables form inputs during loading state", async () => {
let resolveLogin: () => void;
- const loginPromise = new Promise((resolve) => { resolveLogin = resolve; });
+ const loginPromise = new Promise((resolve) => {
+ resolveLogin = resolve;
+ });
const onLogin = vi.fn().mockReturnValue(loginPromise);
const page = createConnectPage(makeCallbacks({ onLogin }), testProfiles);
@@ -269,7 +269,11 @@ describe("ConnectPage", () => {
// --- selectServer / auto-login flow ---
it("selectServer sets host and loads credentials asynchronously", async () => {
- mockLoadCredential.mockResolvedValue({ username: "saveduser", token: "tok", password: "savedpass" });
+ mockLoadCredential.mockResolvedValue({
+ username: "saveduser",
+ token: "tok",
+ password: "savedpass",
+ });
const page = createConnectPage(makeCallbacks(), testProfiles);
page.mount(container);
@@ -284,8 +288,9 @@ describe("ConnectPage", () => {
expect(usernameInput.value).toBe("saveduser");
});
+ // Password is no longer returned from credential store over IPC (security hardening)
const passwordInput = container.querySelector("#password") as HTMLInputElement;
- expect(passwordInput.value).toBe("savedpass");
+ expect(passwordInput.value).toBe("");
page.destroy?.();
});
@@ -627,7 +632,12 @@ describe("ConnectPage", () => {
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
await vi.waitFor(() => {
- expect(onRegister).toHaveBeenCalledWith("localhost:8443", "newuser", "password123", "INVITE-CODE");
+ expect(onRegister).toHaveBeenCalledWith(
+ "localhost:8443",
+ "newuser",
+ "password123",
+ "INVITE-CODE",
+ );
});
page.destroy?.();
@@ -782,7 +792,9 @@ describe("ConnectPage", () => {
it("ignores submit while already loading", async () => {
let resolveLogin: () => void;
- const loginPromise = new Promise((resolve) => { resolveLogin = resolve; });
+ const loginPromise = new Promise((resolve) => {
+ resolveLogin = resolve;
+ });
const onLogin = vi.fn().mockReturnValue(loginPromise);
const page = createConnectPage(makeCallbacks({ onLogin }), testProfiles);
@@ -799,7 +811,9 @@ describe("ConnectPage", () => {
const form = container.querySelector(".connect-form") as HTMLFormElement;
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
- await vi.waitFor(() => { expect(onLogin).toHaveBeenCalledTimes(1); });
+ await vi.waitFor(() => {
+ expect(onLogin).toHaveBeenCalledTimes(1);
+ });
// Submit again while loading
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
@@ -945,7 +959,9 @@ describe("ConnectPage", () => {
it("shows correct button text during loading states", async () => {
let resolveLogin: () => void;
- const loginPromise = new Promise((resolve) => { resolveLogin = resolve; });
+ const loginPromise = new Promise((resolve) => {
+ resolveLogin = resolve;
+ });
const onLogin = vi.fn().mockReturnValue(loginPromise);
const page = createConnectPage(makeCallbacks({ onLogin }), testProfiles);
@@ -973,7 +989,9 @@ describe("ConnectPage", () => {
it("shows registering text in register mode during loading", async () => {
let resolveRegister: () => void;
- const registerPromise = new Promise((resolve) => { resolveRegister = resolve; });
+ const registerPromise = new Promise((resolve) => {
+ resolveRegister = resolve;
+ });
const onRegister = vi.fn().mockReturnValue(registerPromise);
const page = createConnectPage(makeCallbacks({ onRegister }), testProfiles);
diff --git a/Client/tauri-client/tests/unit/connection-stats.test.ts b/Client/tauri-client/tests/unit/connection-stats.test.ts
index 8954a62a..0a190f3b 100644
--- a/Client/tauri-client/tests/unit/connection-stats.test.ts
+++ b/Client/tauri-client/tests/unit/connection-stats.test.ts
@@ -203,7 +203,13 @@ describe("createConnectionStatsPoller", () => {
it("classifies quality as fair for RTT 100-200ms", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.15, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.15,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
]);
const cb = vi.fn();
poller = createConnectionStatsPoller(() => room as any);
@@ -217,7 +223,13 @@ describe("createConnectionStatsPoller", () => {
it("classifies quality as poor for RTT 200-400ms", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.3, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.3,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
]);
const cb = vi.fn();
poller = createConnectionStatsPoller(() => room as any);
@@ -231,7 +243,13 @@ describe("createConnectionStatsPoller", () => {
it("classifies quality as bad for RTT >= 400ms", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.5, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.5,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
]);
const cb = vi.fn();
poller = createConnectionStatsPoller(() => room as any);
@@ -245,7 +263,13 @@ describe("createConnectionStatsPoller", () => {
it("extracts outbound-rtp and inbound-rtp packet counts", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.01, bytesSent: 100, bytesReceived: 200 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.01,
+ bytesSent: 100,
+ bytesReceived: 200,
+ },
{ id: "out1", type: "outbound-rtp", packetsSent: 500, bytesSent: 40000 },
{ id: "in1", type: "inbound-rtp", packetsReceived: 300, bytesReceived: 30000 },
]);
@@ -265,7 +289,13 @@ describe("createConnectionStatsPoller", () => {
const room = createMockRoom([
{ id: "out1", type: "outbound-rtp", packetsSent: 100, bytesSent: 10000 },
{ id: "in1", type: "inbound-rtp", packetsReceived: 50, bytesReceived: 5000 },
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.01, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.01,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
]);
const cb = vi.fn();
poller = createConnectionStatsPoller(() => room as any);
@@ -307,7 +337,7 @@ describe("createConnectionStatsPoller", () => {
};
const qualityCb = vi.fn();
- poller = createConnectionStatsPoller(() => roomActive ? room as any : null);
+ poller = createConnectionStatsPoller(() => (roomActive ? (room as any) : null));
poller.onQualityChanged(qualityCb);
poller.start();
@@ -330,7 +360,13 @@ describe("createConnectionStatsPoller", () => {
it("unsubscribed onUpdate callback is not called", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.01, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.01,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
]);
const cb = vi.fn();
poller = createConnectionStatsPoller(() => room as any);
@@ -468,8 +504,20 @@ describe("createConnectionStatsPoller", () => {
it("ignores candidate-pair entries with non-numeric or zero RTT", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: "bad", bytesSent: 0, bytesReceived: 0 },
- { id: "cp2", type: "candidate-pair", currentRoundTripTime: 0, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: "bad",
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
+ {
+ id: "cp2",
+ type: "candidate-pair",
+ currentRoundTripTime: 0,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
]);
const cb = vi.fn();
poller = createConnectionStatsPoller(() => room as any);
@@ -483,8 +531,20 @@ describe("createConnectionStatsPoller", () => {
it("picks the lowest RTT when multiple candidate-pairs exist", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.3, bytesSent: 0, bytesReceived: 0 },
- { id: "cp2", type: "candidate-pair", currentRoundTripTime: 0.05, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.3,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
+ {
+ id: "cp2",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.05,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
]);
const cb = vi.fn();
poller = createConnectionStatsPoller(() => room as any);
@@ -605,7 +665,13 @@ describe("createConnectionStatsPoller", () => {
it("handles outbound-rtp without packetsSent", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.01, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.01,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
{ id: "out1", type: "outbound-rtp", bytesSent: 100 },
]);
const cb = vi.fn();
@@ -619,7 +685,13 @@ describe("createConnectionStatsPoller", () => {
it("handles inbound-rtp without packetsReceived", async () => {
const room = createMockRoom([
- { id: "cp1", type: "candidate-pair", currentRoundTripTime: 0.01, bytesSent: 0, bytesReceived: 0 },
+ {
+ id: "cp1",
+ type: "candidate-pair",
+ currentRoundTripTime: 0.01,
+ bytesSent: 0,
+ bytesReceived: 0,
+ },
{ id: "in1", type: "inbound-rtp", bytesReceived: 100 },
]);
const cb = vi.fn();
diff --git a/Client/tauri-client/tests/unit/context-menu.test.ts b/Client/tauri-client/tests/unit/context-menu.test.ts
index fc6fa9c5..7c7c7be4 100644
--- a/Client/tauri-client/tests/unit/context-menu.test.ts
+++ b/Client/tauri-client/tests/unit/context-menu.test.ts
@@ -1,97 +1,97 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { showContextMenu } from '../../src/lib/context-menu';
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { showContextMenu } from "../../src/lib/context-menu";
-describe('showContextMenu', () => {
+describe("showContextMenu", () => {
let ac: AbortController;
beforeEach(() => {
ac = new AbortController();
// Clean up any leftover menus
- document.querySelectorAll('.context-menu').forEach((el) => el.remove());
+ document.querySelectorAll(".context-menu").forEach((el) => el.remove());
});
afterEach(() => {
ac.abort();
- document.querySelectorAll('.context-menu').forEach((el) => el.remove());
+ document.querySelectorAll(".context-menu").forEach((el) => el.remove());
});
- it('renders menu at correct position', () => {
+ it("renders menu at correct position", () => {
showContextMenu({
x: 100,
y: 200,
- items: [{ label: 'Test', onClick: vi.fn() }],
+ items: [{ label: "Test", onClick: vi.fn() }],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
+ const menu = document.querySelector(".context-menu") as HTMLElement;
expect(menu).not.toBeNull();
- expect(menu.style.left).toBe('100px');
- expect(menu.style.top).toBe('200px');
+ expect(menu.style.left).toBe("100px");
+ expect(menu.style.top).toBe("200px");
});
- it('renders all items', () => {
+ it("renders all items", () => {
showContextMenu({
x: 0,
y: 0,
items: [
- { label: 'Edit', onClick: vi.fn() },
- { label: 'Delete', onClick: vi.fn(), danger: true },
+ { label: "Edit", onClick: vi.fn() },
+ { label: "Delete", onClick: vi.fn(), danger: true },
],
signal: ac.signal,
});
- const items = document.querySelectorAll('.context-menu-item');
+ const items = document.querySelectorAll(".context-menu-item");
expect(items.length).toBe(2);
- expect(items[0]!.textContent).toBe('Edit');
- expect(items[1]!.textContent).toBe('Delete');
+ expect(items[0]!.textContent).toBe("Edit");
+ expect(items[1]!.textContent).toBe("Delete");
});
- it('fires onClick when item clicked', () => {
+ it("fires onClick when item clicked", () => {
const onClick = vi.fn();
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'Action', onClick }],
+ items: [{ label: "Action", onClick }],
signal: ac.signal,
});
- const item = document.querySelector('.context-menu-item') as HTMLElement;
+ const item = document.querySelector(".context-menu-item") as HTMLElement;
item.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
- it('removes menu after item click', () => {
+ it("removes menu after item click", () => {
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'Action', onClick: vi.fn() }],
+ items: [{ label: "Action", onClick: vi.fn() }],
signal: ac.signal,
});
- const item = document.querySelector('.context-menu-item') as HTMLElement;
+ const item = document.querySelector(".context-menu-item") as HTMLElement;
item.click();
- expect(document.querySelector('.context-menu')).toBeNull();
+ expect(document.querySelector(".context-menu")).toBeNull();
});
- it('applies danger class to danger items', () => {
+ it("applies danger class to danger items", () => {
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'Delete', onClick: vi.fn(), danger: true }],
+ items: [{ label: "Delete", onClick: vi.fn(), danger: true }],
signal: ac.signal,
});
- const item = document.querySelector('.context-menu-item') as HTMLElement;
- expect(item.classList.contains('danger')).toBe(true);
+ const item = document.querySelector(".context-menu-item") as HTMLElement;
+ expect(item.classList.contains("danger")).toBe(true);
});
- it('applies testId to items', () => {
+ it("applies testId to items", () => {
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'Edit', onClick: vi.fn(), testId: 'ctx-edit' }],
+ items: [{ label: "Edit", onClick: vi.fn(), testId: "ctx-edit" }],
signal: ac.signal,
});
@@ -99,162 +99,160 @@ describe('showContextMenu', () => {
expect(item).not.toBeNull();
});
- it('removes menu on AbortSignal abort', () => {
+ it("removes menu on AbortSignal abort", () => {
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'Test', onClick: vi.fn() }],
+ items: [{ label: "Test", onClick: vi.fn() }],
signal: ac.signal,
});
- expect(document.querySelector('.context-menu')).not.toBeNull();
+ expect(document.querySelector(".context-menu")).not.toBeNull();
ac.abort();
- expect(document.querySelector('.context-menu')).toBeNull();
+ expect(document.querySelector(".context-menu")).toBeNull();
});
- it('removes existing menu with same className before showing new one', () => {
+ it("removes existing menu with same className before showing new one", () => {
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'First', onClick: vi.fn() }],
+ items: [{ label: "First", onClick: vi.fn() }],
signal: ac.signal,
- className: 'my-menu',
+ className: "my-menu",
});
showContextMenu({
x: 50,
y: 50,
- items: [{ label: 'Second', onClick: vi.fn() }],
+ items: [{ label: "Second", onClick: vi.fn() }],
signal: ac.signal,
- className: 'my-menu',
+ className: "my-menu",
});
- const menus = document.querySelectorAll('.my-menu');
+ const menus = document.querySelectorAll(".my-menu");
expect(menus.length).toBe(1);
- expect(menus[0]!.querySelector('.context-menu-item')!.textContent).toBe('Second');
+ expect(menus[0]!.querySelector(".context-menu-item")!.textContent).toBe("Second");
});
it('uses default className "context-menu" when none specified', () => {
showContextMenu({
x: 10,
y: 20,
- items: [{ label: 'Default', onClick: vi.fn() }],
+ items: [{ label: "Default", onClick: vi.fn() }],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
+ const menu = document.querySelector(".context-menu") as HTMLElement;
expect(menu).not.toBeNull();
- expect(menu.classList.contains('context-menu')).toBe(true);
+ expect(menu.classList.contains("context-menu")).toBe(true);
});
- it('adds separator before danger item when preceded by non-danger item', () => {
+ it("adds separator before danger item when preceded by non-danger item", () => {
showContextMenu({
x: 0,
y: 0,
items: [
- { label: 'Edit', onClick: vi.fn() },
- { label: 'Delete', onClick: vi.fn(), danger: true },
+ { label: "Edit", onClick: vi.fn() },
+ { label: "Delete", onClick: vi.fn(), danger: true },
],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
- const sep = menu.querySelector('.context-menu-sep');
+ const menu = document.querySelector(".context-menu") as HTMLElement;
+ const sep = menu.querySelector(".context-menu-sep");
expect(sep).not.toBeNull();
});
- it('does not add separator when danger item is first', () => {
+ it("does not add separator when danger item is first", () => {
showContextMenu({
x: 0,
y: 0,
- items: [
- { label: 'Delete', onClick: vi.fn(), danger: true },
- ],
+ items: [{ label: "Delete", onClick: vi.fn(), danger: true }],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
- const sep = menu.querySelector('.context-menu-sep');
+ const menu = document.querySelector(".context-menu") as HTMLElement;
+ const sep = menu.querySelector(".context-menu-sep");
expect(sep).toBeNull();
});
- it('does not add separator between consecutive danger items', () => {
+ it("does not add separator between consecutive danger items", () => {
showContextMenu({
x: 0,
y: 0,
items: [
- { label: 'Delete', onClick: vi.fn(), danger: true },
- { label: 'Ban', onClick: vi.fn(), danger: true },
+ { label: "Delete", onClick: vi.fn(), danger: true },
+ { label: "Ban", onClick: vi.fn(), danger: true },
],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
- const seps = menu.querySelectorAll('.context-menu-sep');
+ const menu = document.querySelector(".context-menu") as HTMLElement;
+ const seps = menu.querySelectorAll(".context-menu-sep");
expect(seps.length).toBe(0);
});
- it('handles multiple non-danger items without separator', () => {
+ it("handles multiple non-danger items without separator", () => {
showContextMenu({
x: 0,
y: 0,
items: [
- { label: 'Copy', onClick: vi.fn() },
- { label: 'Edit', onClick: vi.fn() },
- { label: 'Reply', onClick: vi.fn() },
+ { label: "Copy", onClick: vi.fn() },
+ { label: "Edit", onClick: vi.fn() },
+ { label: "Reply", onClick: vi.fn() },
],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
- const seps = menu.querySelectorAll('.context-menu-sep');
+ const menu = document.querySelector(".context-menu") as HTMLElement;
+ const seps = menu.querySelectorAll(".context-menu-sep");
expect(seps.length).toBe(0);
});
- it('closes menu on click outside (mousedown)', async () => {
+ it("closes menu on click outside (mousedown)", async () => {
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'Test', onClick: vi.fn() }],
+ items: [{ label: "Test", onClick: vi.fn() }],
signal: ac.signal,
});
- expect(document.querySelector('.context-menu')).not.toBeNull();
+ expect(document.querySelector(".context-menu")).not.toBeNull();
// Trigger the deferred mousedown listener (needs setTimeout to fire first)
await vi.waitFor(() => {
// Simulate a click outside the menu
- document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
- expect(document.querySelector('.context-menu')).toBeNull();
+ document.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ expect(document.querySelector(".context-menu")).toBeNull();
});
});
- it('does not close menu on mousedown inside the menu', async () => {
+ it("does not close menu on mousedown inside the menu", async () => {
showContextMenu({
x: 0,
y: 0,
- items: [{ label: 'Test', onClick: vi.fn() }],
+ items: [{ label: "Test", onClick: vi.fn() }],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
+ const menu = document.querySelector(".context-menu") as HTMLElement;
expect(menu).not.toBeNull();
// Wait for the deferred listener to register
await new Promise((r) => setTimeout(r, 10));
// Mousedown inside the menu should NOT close it
- const item = menu.querySelector('.context-menu-item') as HTMLElement;
- const event = new MouseEvent('mousedown', { bubbles: true });
- Object.defineProperty(event, 'target', { value: item });
+ const item = menu.querySelector(".context-menu-item") as HTMLElement;
+ const event = new MouseEvent("mousedown", { bubbles: true });
+ Object.defineProperty(event, "target", { value: item });
document.dispatchEvent(event);
- expect(document.querySelector('.context-menu')).not.toBeNull();
+ expect(document.querySelector(".context-menu")).not.toBeNull();
});
- it('handles empty items list', () => {
+ it("handles empty items list", () => {
showContextMenu({
x: 0,
y: 0,
@@ -262,59 +260,59 @@ describe('showContextMenu', () => {
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
+ const menu = document.querySelector(".context-menu") as HTMLElement;
expect(menu).not.toBeNull();
- expect(menu.querySelectorAll('.context-menu-item').length).toBe(0);
+ expect(menu.querySelectorAll(".context-menu-item").length).toBe(0);
});
- it('clicking one item does not affect other items', () => {
+ it("clicking one item does not affect other items", () => {
const onClick1 = vi.fn();
const onClick2 = vi.fn();
showContextMenu({
x: 0,
y: 0,
items: [
- { label: 'Action1', onClick: onClick1 },
- { label: 'Action2', onClick: onClick2 },
+ { label: "Action1", onClick: onClick1 },
+ { label: "Action2", onClick: onClick2 },
],
signal: ac.signal,
});
- const items = document.querySelectorAll('.context-menu-item');
+ const items = document.querySelectorAll(".context-menu-item");
(items[0] as HTMLElement).click();
expect(onClick1).toHaveBeenCalledTimes(1);
expect(onClick2).not.toHaveBeenCalled();
});
- it('handles non-danger item followed by danger item with separator', () => {
+ it("handles non-danger item followed by danger item with separator", () => {
showContextMenu({
x: 0,
y: 0,
items: [
- { label: 'Copy', onClick: vi.fn() },
- { label: 'Edit', onClick: vi.fn() },
- { label: 'Delete', onClick: vi.fn(), danger: true },
+ { label: "Copy", onClick: vi.fn() },
+ { label: "Edit", onClick: vi.fn() },
+ { label: "Delete", onClick: vi.fn(), danger: true },
],
signal: ac.signal,
});
- const menu = document.querySelector('.context-menu') as HTMLElement;
+ const menu = document.querySelector(".context-menu") as HTMLElement;
const children = Array.from(menu.children);
// Should have: Copy, Edit, separator, Delete
expect(children.length).toBe(4);
- expect(children[2]!.classList.contains('context-menu-sep')).toBe(true);
+ expect(children[2]!.classList.contains("context-menu-sep")).toBe(true);
});
- it('menu is appended to document.body', () => {
+ it("menu is appended to document.body", () => {
showContextMenu({
x: 100,
y: 200,
- items: [{ label: 'Appended', onClick: vi.fn() }],
+ items: [{ label: "Appended", onClick: vi.fn() }],
signal: ac.signal,
});
- const menu = document.body.querySelector('.context-menu');
+ const menu = document.body.querySelector(".context-menu");
expect(menu).not.toBeNull();
});
});
diff --git a/Client/tauri-client/tests/unit/create-channel-modal.test.ts b/Client/tauri-client/tests/unit/create-channel-modal.test.ts
index e73628c6..8e079d0f 100644
--- a/Client/tauri-client/tests/unit/create-channel-modal.test.ts
+++ b/Client/tauri-client/tests/unit/create-channel-modal.test.ts
@@ -42,17 +42,11 @@ describe("allowedTypesForCategory", () => {
});
it("returns text and announcement for text categories", () => {
- expect(allowedTypesForCategory("Text Channels")).toEqual([
- "text",
- "announcement",
- ]);
+ expect(allowedTypesForCategory("Text Channels")).toEqual(["text", "announcement"]);
});
it("returns text and announcement for 'Chat'", () => {
- expect(allowedTypesForCategory("Chat")).toEqual([
- "text",
- "announcement",
- ]);
+ expect(allowedTypesForCategory("Chat")).toEqual(["text", "announcement"]);
});
});
@@ -94,7 +88,9 @@ describe("CreateChannelModal", () => {
it("shows only text and announcement types for text categories", () => {
const { modal } = makeModal("Text Channels");
- const select = container.querySelector("[data-testid='channel-type-select']") as HTMLSelectElement;
+ const select = container.querySelector(
+ "[data-testid='channel-type-select']",
+ ) as HTMLSelectElement;
const options = Array.from(select.options).map((o) => o.value);
expect(options).toEqual(["text", "announcement"]);
expect(options).not.toContain("voice");
@@ -103,7 +99,9 @@ describe("CreateChannelModal", () => {
it("shows only voice type for voice categories", () => {
const { modal } = makeModal("Voice Channels");
- const select = container.querySelector("[data-testid='channel-type-select']") as HTMLSelectElement;
+ const select = container.querySelector(
+ "[data-testid='channel-type-select']",
+ ) as HTMLSelectElement;
const options = Array.from(select.options).map((o) => o.value);
expect(options).toEqual(["voice"]);
expect(options).not.toContain("text");
@@ -121,7 +119,9 @@ describe("CreateChannelModal", () => {
const onCreate = vi.fn(async () => {});
const { modal } = makeModal("Text Channels", { onCreate });
- const submitBtn = container.querySelector("[data-testid='channel-create-submit']") as HTMLButtonElement;
+ const submitBtn = container.querySelector(
+ "[data-testid='channel-create-submit']",
+ ) as HTMLButtonElement;
submitBtn.click();
const error = container.querySelector("[data-testid='channel-create-error']");
@@ -134,10 +134,14 @@ describe("CreateChannelModal", () => {
const onCreate = vi.fn(async () => {});
const { modal } = makeModal("Text Channels", { onCreate });
- const nameInput = container.querySelector("[data-testid='channel-name-input']") as HTMLInputElement;
+ const nameInput = container.querySelector(
+ "[data-testid='channel-name-input']",
+ ) as HTMLInputElement;
nameInput.value = "test-channel";
- const submitBtn = container.querySelector("[data-testid='channel-create-submit']") as HTMLButtonElement;
+ const submitBtn = container.querySelector(
+ "[data-testid='channel-create-submit']",
+ ) as HTMLButtonElement;
submitBtn.click();
// Wait for async handler
@@ -174,10 +178,14 @@ describe("CreateChannelModal", () => {
const onCreate = vi.fn().mockRejectedValue(new Error("Name already exists"));
const { modal } = makeModal("Text Channels", { onCreate });
- const nameInput = container.querySelector("[data-testid='channel-name-input']") as HTMLInputElement;
+ const nameInput = container.querySelector(
+ "[data-testid='channel-name-input']",
+ ) as HTMLInputElement;
nameInput.value = "duplicate";
- const submitBtn = container.querySelector("[data-testid='channel-create-submit']") as HTMLButtonElement;
+ const submitBtn = container.querySelector(
+ "[data-testid='channel-create-submit']",
+ ) as HTMLButtonElement;
submitBtn.click();
await vi.waitFor(() => {
@@ -196,10 +204,14 @@ describe("CreateChannelModal", () => {
const onCreate = vi.fn().mockRejectedValue("string error");
const { modal } = makeModal("Text Channels", { onCreate });
- const nameInput = container.querySelector("[data-testid='channel-name-input']") as HTMLInputElement;
+ const nameInput = container.querySelector(
+ "[data-testid='channel-name-input']",
+ ) as HTMLInputElement;
nameInput.value = "test";
- const submitBtn = container.querySelector("[data-testid='channel-create-submit']") as HTMLButtonElement;
+ const submitBtn = container.querySelector(
+ "[data-testid='channel-create-submit']",
+ ) as HTMLButtonElement;
submitBtn.click();
await vi.waitFor(() => {
@@ -212,13 +224,22 @@ describe("CreateChannelModal", () => {
it("disables submit button and shows 'Creating...' while creating", async () => {
let resolveCreate: (() => void) | undefined;
- const onCreate = vi.fn(() => new Promise((resolve) => { resolveCreate = resolve; }));
+ const onCreate = vi.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveCreate = resolve;
+ }),
+ );
const { modal } = makeModal("Text Channels", { onCreate });
- const nameInput = container.querySelector("[data-testid='channel-name-input']") as HTMLInputElement;
+ const nameInput = container.querySelector(
+ "[data-testid='channel-name-input']",
+ ) as HTMLInputElement;
nameInput.value = "new-channel";
- const submitBtn = container.querySelector("[data-testid='channel-create-submit']") as HTMLButtonElement;
+ const submitBtn = container.querySelector(
+ "[data-testid='channel-create-submit']",
+ ) as HTMLButtonElement;
submitBtn.click();
expect(submitBtn.hasAttribute("disabled")).toBe(true);
@@ -243,7 +264,9 @@ describe("CreateChannelModal", () => {
const onClose = vi.fn();
const { modal } = makeModal("Text Channels", { onClose });
- const overlay = container.querySelector("[data-testid='create-channel-modal']") as HTMLDivElement;
+ const overlay = container.querySelector(
+ "[data-testid='create-channel-modal']",
+ ) as HTMLDivElement;
// Simulate clicking the overlay backdrop directly
overlay.dispatchEvent(new MouseEvent("click", { bubbles: true }));
@@ -252,19 +275,26 @@ describe("CreateChannelModal", () => {
});
it("clears previous error when submitting valid data after error", async () => {
- const onCreate = vi.fn()
+ const onCreate = vi
+ .fn()
.mockRejectedValueOnce(new Error("First error"))
.mockResolvedValueOnce(undefined);
const { modal } = makeModal("Text Channels", { onCreate });
- const nameInput = container.querySelector("[data-testid='channel-name-input']") as HTMLInputElement;
+ const nameInput = container.querySelector(
+ "[data-testid='channel-name-input']",
+ ) as HTMLInputElement;
nameInput.value = "test";
- const submitBtn = container.querySelector("[data-testid='channel-create-submit']") as HTMLButtonElement;
+ const submitBtn = container.querySelector(
+ "[data-testid='channel-create-submit']",
+ ) as HTMLButtonElement;
submitBtn.click();
await vi.waitFor(() => {
- expect(container.querySelector("[data-testid='channel-create-error']")?.textContent).toBe("First error");
+ expect(container.querySelector("[data-testid='channel-create-error']")?.textContent).toBe(
+ "First error",
+ );
});
// Try again with a valid name
diff --git a/Client/tauri-client/tests/unit/delete-channel-modal.test.ts b/Client/tauri-client/tests/unit/delete-channel-modal.test.ts
index 2e754583..fccfc32b 100644
--- a/Client/tauri-client/tests/unit/delete-channel-modal.test.ts
+++ b/Client/tauri-client/tests/unit/delete-channel-modal.test.ts
@@ -50,7 +50,9 @@ describe("DeleteChannelModal", () => {
it("calls onConfirm when delete button is clicked", async () => {
const onConfirm = vi.fn(async () => {});
const { modal } = makeModal({ onConfirm });
- const deleteBtn = container.querySelector("[data-testid='delete-channel-confirm']") as HTMLButtonElement;
+ const deleteBtn = container.querySelector(
+ "[data-testid='delete-channel-confirm']",
+ ) as HTMLButtonElement;
deleteBtn.click();
await vi.waitFor(() => {
@@ -88,7 +90,9 @@ describe("DeleteChannelModal", () => {
const onConfirm = vi.fn().mockRejectedValue(new Error("Permission denied"));
const { modal } = makeModal({ onConfirm });
- const deleteBtn = container.querySelector("[data-testid='delete-channel-confirm']") as HTMLButtonElement;
+ const deleteBtn = container.querySelector(
+ "[data-testid='delete-channel-confirm']",
+ ) as HTMLButtonElement;
deleteBtn.click();
await vi.waitFor(() => {
@@ -106,7 +110,9 @@ describe("DeleteChannelModal", () => {
const onConfirm = vi.fn().mockRejectedValue(42);
const { modal } = makeModal({ onConfirm });
- const deleteBtn = container.querySelector("[data-testid='delete-channel-confirm']") as HTMLButtonElement;
+ const deleteBtn = container.querySelector(
+ "[data-testid='delete-channel-confirm']",
+ ) as HTMLButtonElement;
deleteBtn.click();
await vi.waitFor(() => {
@@ -119,10 +125,17 @@ describe("DeleteChannelModal", () => {
it("disables button and shows 'Deleting...' during delete", async () => {
let resolveDelete: (() => void) | undefined;
- const onConfirm = vi.fn(() => new Promise((resolve) => { resolveDelete = resolve; }));
+ const onConfirm = vi.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveDelete = resolve;
+ }),
+ );
const { modal } = makeModal({ onConfirm });
- const deleteBtn = container.querySelector("[data-testid='delete-channel-confirm']") as HTMLButtonElement;
+ const deleteBtn = container.querySelector(
+ "[data-testid='delete-channel-confirm']",
+ ) as HTMLButtonElement;
deleteBtn.click();
expect(deleteBtn.hasAttribute("disabled")).toBe(true);
@@ -136,7 +149,9 @@ describe("DeleteChannelModal", () => {
const onClose = vi.fn();
const { modal } = makeModal({ onClose });
- const overlay = container.querySelector("[data-testid='delete-channel-modal']") as HTMLDivElement;
+ const overlay = container.querySelector(
+ "[data-testid='delete-channel-modal']",
+ ) as HTMLDivElement;
overlay.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onClose).toHaveBeenCalled();
diff --git a/Client/tauri-client/tests/unit/device-manager.test.ts b/Client/tauri-client/tests/unit/device-manager.test.ts
index 31498420..44c37c10 100644
--- a/Client/tauri-client/tests/unit/device-manager.test.ts
+++ b/Client/tauri-client/tests/unit/device-manager.test.ts
@@ -106,14 +106,22 @@ describe("DeviceManager", () => {
describe("setAudioPipeline", () => {
it("accepts null to clear the pipeline", () => {
- const pipeline = { setupAudioPipeline: vi.fn(), applyNoiseSuppressor: vi.fn(), removeNoiseSuppressor: vi.fn() } as any;
+ const pipeline = {
+ setupAudioPipeline: vi.fn(),
+ applyNoiseSuppressor: vi.fn(),
+ removeNoiseSuppressor: vi.fn(),
+ } as any;
dm.setAudioPipeline(pipeline);
dm.setAudioPipeline(null);
// After clearing, pipeline methods should not be called on device switch
});
it("stores a pipeline object for use during device switches", () => {
- const pipeline = { setupAudioPipeline: vi.fn(), applyNoiseSuppressor: vi.fn(), removeNoiseSuppressor: vi.fn() } as any;
+ const pipeline = {
+ setupAudioPipeline: vi.fn(),
+ applyNoiseSuppressor: vi.fn(),
+ removeNoiseSuppressor: vi.fn(),
+ } as any;
dm.setAudioPipeline(pipeline);
// Pipeline is stored internally — integration with switchInputDevice tested below
});
@@ -212,7 +220,9 @@ describe("DeviceManager", () => {
it("shows toast when pipeline setup fails", async () => {
const onToast = vi.fn();
const pipeline = {
- setupAudioPipeline: vi.fn(() => { throw new Error("pipeline error"); }),
+ setupAudioPipeline: vi.fn(() => {
+ throw new Error("pipeline error");
+ }),
applyNoiseSuppressor: vi.fn().mockResolvedValue(undefined),
removeNoiseSuppressor: vi.fn().mockResolvedValue(undefined),
} as any;
@@ -338,7 +348,9 @@ describe("DeviceManager", () => {
await vi.advanceTimersByTimeAsync(600);
expect(mockSavePref).toHaveBeenCalledWith("audioOutputDevice", "");
- expect(onToast).toHaveBeenCalledWith("Audio output device disconnected — switched to default");
+ expect(onToast).toHaveBeenCalledWith(
+ "Audio output device disconnected — switched to default",
+ );
});
it("calls onError when mic fallback fails", async () => {
@@ -397,7 +409,9 @@ describe("DeviceManager", () => {
});
const pipeline = {
- setupAudioPipeline: vi.fn(() => { throw new Error("pipeline error"); }),
+ setupAudioPipeline: vi.fn(() => {
+ throw new Error("pipeline error");
+ }),
} as any;
const onToast = vi.fn();
diff --git a/Client/tauri-client/tests/unit/dispatcher.test.ts b/Client/tauri-client/tests/unit/dispatcher.test.ts
index 9bae0588..0003541b 100644
--- a/Client/tauri-client/tests/unit/dispatcher.test.ts
+++ b/Client/tauri-client/tests/unit/dispatcher.test.ts
@@ -13,6 +13,7 @@ import type { ServerMessage } from "../../src/lib/types";
// Mock notifications and livekitSession to avoid side effects
vi.mock("@lib/notifications", () => ({
notifyIncomingMessage: vi.fn(),
+ cleanupNotificationAudio: vi.fn(),
}));
vi.mock("@lib/livekitSession", () => ({
handleVoiceToken: vi.fn(async () => {}),
@@ -40,10 +41,7 @@ function createMockWs() {
connect: vi.fn(),
disconnect: vi.fn(),
send: vi.fn(() => "test-id"),
- on(
- type: T,
- listener: WsListener,
- ): () => void {
+ on(type: T, listener: WsListener): () => void {
if (!listeners.has(type)) {
listeners.set(type, new Set());
}
@@ -110,7 +108,7 @@ describe("WS Dispatcher", () => {
localCamera: false,
localScreenshare: false,
joinedAt: null,
- listenOnly: false,
+ listenOnly: false,
}));
dmStore.setState(() => ({ channels: [] }));
uiStore.setState((prev) => ({ ...prev, transientError: null }));
@@ -148,12 +146,8 @@ describe("WS Dispatcher", () => {
{ id: 1, name: "general", type: "text", category: null, position: 0 },
{ id: 2, name: "voice", type: "voice", category: null, position: 1 },
],
- members: [
- { id: 1, username: "alex", avatar: null, role: "admin", status: "online" },
- ],
- voice_states: [
- { channel_id: 2, user_id: 1, muted: false, deafened: false },
- ],
+ members: [{ id: 1, username: "alex", avatar: null, role: "admin", status: "online" }],
+ voice_states: [{ channel_id: 2, user_id: 1, muted: false, deafened: false }],
roles: [],
});
@@ -286,7 +280,13 @@ describe("WS Dispatcher", () => {
it("wires member_ban to remove member from members store", () => {
membersStore.setState((prev) => {
const m = new Map(prev.members);
- m.set(77, { id: 77, username: "banned-user", avatar: null, role: "member", status: "online" as const });
+ m.set(77, {
+ id: 77,
+ username: "banned-user",
+ avatar: null,
+ role: "member",
+ status: "online" as const,
+ });
return { ...prev, members: m };
});
@@ -297,7 +297,13 @@ describe("WS Dispatcher", () => {
it("wires member_leave to members store", () => {
membersStore.setState((prev) => {
const m = new Map(prev.members);
- m.set(99, { id: 99, username: "bye", avatar: null, role: "member", status: "online" as const });
+ m.set(99, {
+ id: 99,
+ username: "bye",
+ avatar: null,
+ role: "member",
+ status: "online" as const,
+ });
return { ...prev, members: m };
});
@@ -323,12 +329,10 @@ describe("WS Dispatcher", () => {
it("wires ready with DM channels in payload", () => {
mock.dispatch("ready", {
- channels: [
- { id: 1, name: "general", type: "text", category: null, position: 0 },
- ],
+ channels: [{ id: 1, name: "general", type: "text", category: null, position: 0 }],
members: [],
voice_states: [],
- roles: [{ id: 1, name: "admin", permissions: 0x7FFFFFFF }],
+ roles: [{ id: 1, name: "admin", permissions: 0x7fffffff }],
dm_channels: [
{
channel_id: 100,
@@ -369,9 +373,7 @@ describe("WS Dispatcher", () => {
}));
mock.dispatch("ready", {
- channels: [
- { id: 1, name: "general", type: "text", category: null, position: 0 },
- ],
+ channels: [{ id: 1, name: "general", type: "text", category: null, position: 0 }],
members: [],
voice_states: [],
roles: [],
@@ -382,9 +384,7 @@ describe("WS Dispatcher", () => {
it("ready with no text channels does not set active", () => {
mock.dispatch("ready", {
- channels: [
- { id: 5, name: "voice-only", type: "voice", category: null, position: 0 },
- ],
+ channels: [{ id: 5, name: "voice-only", type: "voice", category: null, position: 0 }],
members: [],
voice_states: [],
roles: [],
@@ -564,7 +564,13 @@ describe("WS Dispatcher", () => {
it("wires member_update to update role", () => {
membersStore.setState((prev) => {
const m = new Map(prev.members);
- m.set(42, { id: 42, username: "alice", avatar: null, role: "member", status: "online" as const });
+ m.set(42, {
+ id: 42,
+ username: "alice",
+ avatar: null,
+ role: "member",
+ status: "online" as const,
+ });
return { ...prev, members: m };
});
@@ -1104,7 +1110,7 @@ describe("WS Dispatcher", () => {
// voice_leave should NOT be sent — the LiveKit room is active
const sendCalls = (mock.ws.send as ReturnType).mock.calls;
const voiceLeaveSent = sendCalls.some(
- ([msg]: [{ type: string }]) => msg.type === "voice_leave",
+ (args: unknown[]) => (args[0] as Record)?.type === "voice_leave",
);
expect(voiceLeaveSent).toBe(false);
});
@@ -1131,7 +1137,7 @@ describe("WS Dispatcher", () => {
// voice_leave should NOT be sent — user 42 is not in voice_states
const sendCalls = (mock.ws.send as ReturnType).mock.calls;
const voiceLeaveSent = sendCalls.some(
- ([msg]: [{ type: string }]) => msg.type === "voice_leave",
+ (args: unknown[]) => (args[0] as Record)?.type === "voice_leave",
);
expect(voiceLeaveSent).toBe(false);
});
diff --git a/Client/tauri-client/tests/unit/dm-store.test.ts b/Client/tauri-client/tests/unit/dm-store.test.ts
index 15015117..88160427 100644
--- a/Client/tauri-client/tests/unit/dm-store.test.ts
+++ b/Client/tauri-client/tests/unit/dm-store.test.ts
@@ -65,10 +65,7 @@ describe("dmStore", () => {
});
it("updates an existing channel without creating a duplicate", () => {
- setDmChannels([
- makeDm({ channelId: 1, lastMessage: "old" }),
- makeDm({ channelId: 2 }),
- ]);
+ setDmChannels([makeDm({ channelId: 1, lastMessage: "old" }), makeDm({ channelId: 2 })]);
addDmChannel(makeDm({ channelId: 1, lastMessage: "new" }));
const channels = dmStore.getState().channels;
expect(channels).toHaveLength(2);
@@ -78,11 +75,7 @@ describe("dmStore", () => {
});
it("moves an updated existing channel to the front", () => {
- setDmChannels([
- makeDm({ channelId: 1 }),
- makeDm({ channelId: 2 }),
- makeDm({ channelId: 3 }),
- ]);
+ setDmChannels([makeDm({ channelId: 1 }), makeDm({ channelId: 2 }), makeDm({ channelId: 3 })]);
addDmChannel(makeDm({ channelId: 3, lastMessage: "bumped" }));
const channels = dmStore.getState().channels;
expect(channels[0]!.channelId).toBe(3);
@@ -168,11 +161,7 @@ describe("dmStore", () => {
});
it("moves the updated channel to the front of the list", () => {
- setDmChannels([
- makeDm({ channelId: 1 }),
- makeDm({ channelId: 2 }),
- makeDm({ channelId: 3 }),
- ]);
+ setDmChannels([makeDm({ channelId: 1 }), makeDm({ channelId: 2 }), makeDm({ channelId: 3 })]);
updateDmLastMessagePreview(3, 50, "latest", "2026-03-28T14:00:00Z");
const channels = dmStore.getState().channels;
expect(channels[0]!.channelId).toBe(3);
@@ -213,11 +202,7 @@ describe("dmStore", () => {
describe("updateDmLastMessage — reordering", () => {
it("moves the updated channel to the front of the list", () => {
- setDmChannels([
- makeDm({ channelId: 1 }),
- makeDm({ channelId: 2 }),
- makeDm({ channelId: 3 }),
- ]);
+ setDmChannels([makeDm({ channelId: 1 }), makeDm({ channelId: 2 }), makeDm({ channelId: 3 })]);
updateDmLastMessage(3, 50, "new", "2026-03-28T14:00:00Z");
const channels = dmStore.getState().channels;
expect(channels[0]!.channelId).toBe(3);
diff --git a/Client/tauri-client/tests/unit/edit-channel-modal.test.ts b/Client/tauri-client/tests/unit/edit-channel-modal.test.ts
index 37bf033d..4b201c5f 100644
--- a/Client/tauri-client/tests/unit/edit-channel-modal.test.ts
+++ b/Client/tauri-client/tests/unit/edit-channel-modal.test.ts
@@ -36,7 +36,9 @@ describe("EditChannelModal", () => {
it("pre-fills the name input with current channel name", () => {
const { modal } = makeModal();
- const input = container.querySelector("[data-testid='edit-channel-name-input']") as HTMLInputElement;
+ const input = container.querySelector(
+ "[data-testid='edit-channel-name-input']",
+ ) as HTMLInputElement;
expect(input.value).toBe("general");
modal.destroy?.();
});
@@ -51,10 +53,14 @@ describe("EditChannelModal", () => {
it("shows error when saving with empty name", () => {
const onSave = vi.fn(async () => {});
const { modal } = makeModal({ onSave });
- const input = container.querySelector("[data-testid='edit-channel-name-input']") as HTMLInputElement;
+ const input = container.querySelector(
+ "[data-testid='edit-channel-name-input']",
+ ) as HTMLInputElement;
input.value = "";
- const saveBtn = container.querySelector("[data-testid='edit-channel-submit']") as HTMLButtonElement;
+ const saveBtn = container.querySelector(
+ "[data-testid='edit-channel-submit']",
+ ) as HTMLButtonElement;
saveBtn.click();
const error = container.querySelector("[data-testid='edit-channel-error']");
@@ -66,10 +72,14 @@ describe("EditChannelModal", () => {
it("calls onSave with updated name", async () => {
const onSave = vi.fn(async () => {});
const { modal } = makeModal({ onSave });
- const input = container.querySelector("[data-testid='edit-channel-name-input']") as HTMLInputElement;
+ const input = container.querySelector(
+ "[data-testid='edit-channel-name-input']",
+ ) as HTMLInputElement;
input.value = "renamed-channel";
- const saveBtn = container.querySelector("[data-testid='edit-channel-submit']") as HTMLButtonElement;
+ const saveBtn = container.querySelector(
+ "[data-testid='edit-channel-submit']",
+ ) as HTMLButtonElement;
saveBtn.click();
await vi.waitFor(() => {
@@ -98,10 +108,14 @@ describe("EditChannelModal", () => {
const onSave = vi.fn().mockRejectedValue(new Error("Server error"));
const { modal } = makeModal({ onSave });
- const input = container.querySelector("[data-testid='edit-channel-name-input']") as HTMLInputElement;
+ const input = container.querySelector(
+ "[data-testid='edit-channel-name-input']",
+ ) as HTMLInputElement;
input.value = "new-name";
- const saveBtn = container.querySelector("[data-testid='edit-channel-submit']") as HTMLButtonElement;
+ const saveBtn = container.querySelector(
+ "[data-testid='edit-channel-submit']",
+ ) as HTMLButtonElement;
saveBtn.click();
await vi.waitFor(() => {
@@ -120,10 +134,14 @@ describe("EditChannelModal", () => {
const onSave = vi.fn().mockRejectedValue("unknown");
const { modal } = makeModal({ onSave });
- const input = container.querySelector("[data-testid='edit-channel-name-input']") as HTMLInputElement;
+ const input = container.querySelector(
+ "[data-testid='edit-channel-name-input']",
+ ) as HTMLInputElement;
input.value = "new-name";
- const saveBtn = container.querySelector("[data-testid='edit-channel-submit']") as HTMLButtonElement;
+ const saveBtn = container.querySelector(
+ "[data-testid='edit-channel-submit']",
+ ) as HTMLButtonElement;
saveBtn.click();
await vi.waitFor(() => {
@@ -136,13 +154,22 @@ describe("EditChannelModal", () => {
it("disables save button and shows 'Saving...' during save", async () => {
let resolveSave: (() => void) | undefined;
- const onSave = vi.fn(() => new Promise((resolve) => { resolveSave = resolve; }));
+ const onSave = vi.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveSave = resolve;
+ }),
+ );
const { modal } = makeModal({ onSave });
- const input = container.querySelector("[data-testid='edit-channel-name-input']") as HTMLInputElement;
+ const input = container.querySelector(
+ "[data-testid='edit-channel-name-input']",
+ ) as HTMLInputElement;
input.value = "renamed";
- const saveBtn = container.querySelector("[data-testid='edit-channel-submit']") as HTMLButtonElement;
+ const saveBtn = container.querySelector(
+ "[data-testid='edit-channel-submit']",
+ ) as HTMLButtonElement;
saveBtn.click();
expect(saveBtn.hasAttribute("disabled")).toBe(true);
@@ -178,10 +205,14 @@ describe("EditChannelModal", () => {
const onSave = vi.fn(async () => {});
const { modal } = makeModal({ onSave });
- const input = container.querySelector("[data-testid='edit-channel-name-input']") as HTMLInputElement;
+ const input = container.querySelector(
+ "[data-testid='edit-channel-name-input']",
+ ) as HTMLInputElement;
input.value = " "; // whitespace only
- const saveBtn = container.querySelector("[data-testid='edit-channel-submit']") as HTMLButtonElement;
+ const saveBtn = container.querySelector(
+ "[data-testid='edit-channel-submit']",
+ ) as HTMLButtonElement;
saveBtn.click();
expect(input.classList.contains("error")).toBe(true);
diff --git a/Client/tauri-client/tests/unit/embeds.test.ts b/Client/tauri-client/tests/unit/embeds.test.ts
index a448a0fc..bfefbc16 100644
--- a/Client/tauri-client/tests/unit/embeds.test.ts
+++ b/Client/tauri-client/tests/unit/embeds.test.ts
@@ -1,11 +1,4 @@
-import {
- afterEach,
- beforeEach,
- describe,
- expect,
- it,
- vi,
-} from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { fetchMock } = vi.hoisted(() => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -24,7 +17,12 @@ vi.mock("@lib/media-visibility", () => ({
observeMedia: mockObserveMedia,
}));
-import { clearEmbedCaches, renderGenericLinkPreview, parseOgTags, applyOgMeta } from "../../src/components/message-list/embeds";
+import {
+ clearEmbedCaches,
+ renderGenericLinkPreview,
+ parseOgTags,
+ applyOgMeta,
+} from "../../src/components/message-list/embeds";
import type { OgMeta } from "../../src/components/message-list/embeds";
import { setServerHost } from "../../src/components/message-list/attachments";
@@ -54,9 +52,12 @@ describe("renderGenericLinkPreview", () => {
it("does not reuse OG metadata that resolves after the cache was cleared", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let resolveFetch: ((value: any) => void) | null = null;
- fetchMock.mockImplementationOnce((() => new Promise((resolve) => {
- resolveFetch = resolve;
- })) as any);
+ fetchMock.mockImplementationOnce(
+ (() =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ })) as any,
+ );
const first = renderGenericLinkPreview("https://news.example.com/post");
document.body.appendChild(first);
@@ -67,7 +68,9 @@ describe("renderGenericLinkPreview", () => {
await Promise.resolve();
await Promise.resolve();
- fetchMock.mockResolvedValueOnce(mockHtmlResponse("Fresh"));
+ fetchMock.mockResolvedValueOnce(
+ mockHtmlResponse("Fresh"),
+ );
const second = renderGenericLinkPreview("https://news.example.com/post");
document.body.appendChild(second);
@@ -79,9 +82,12 @@ describe("renderGenericLinkPreview", () => {
it("does not reuse an EMPTY_OG result that resolves after the cache was cleared", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let resolveFetch: ((value: any) => void) | null = null;
- fetchMock.mockImplementationOnce((() => new Promise((resolve) => {
- resolveFetch = resolve;
- })) as any);
+ fetchMock.mockImplementationOnce(
+ (() =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ })) as any,
+ );
const first = renderGenericLinkPreview("https://news.example.com/empty");
document.body.appendChild(first);
@@ -95,7 +101,9 @@ describe("renderGenericLinkPreview", () => {
await Promise.resolve();
await Promise.resolve();
- fetchMock.mockResolvedValueOnce(mockHtmlResponse("Recovered"));
+ fetchMock.mockResolvedValueOnce(
+ mockHtmlResponse("Recovered"),
+ );
const second = renderGenericLinkPreview("https://news.example.com/empty");
document.body.appendChild(second);
@@ -110,12 +118,18 @@ describe("renderGenericLinkPreview", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let resolveSecond: ((value: any) => void) | null = null;
fetchMock
- .mockImplementationOnce((() => new Promise((resolve) => {
- resolveFirst = resolve;
- })) as any)
- .mockImplementationOnce((() => new Promise((resolve) => {
- resolveSecond = resolve;
- })) as any);
+ .mockImplementationOnce(
+ (() =>
+ new Promise((resolve) => {
+ resolveFirst = resolve;
+ })) as any,
+ )
+ .mockImplementationOnce(
+ (() =>
+ new Promise((resolve) => {
+ resolveSecond = resolve;
+ })) as any,
+ );
const first = renderGenericLinkPreview("https://news.example.com/race");
document.body.appendChild(first);
@@ -144,7 +158,9 @@ describe("renderGenericLinkPreview", () => {
});
it("fetches OG metadata for public domains that begin with fd", async () => {
- fetchMock.mockResolvedValue(mockHtmlResponse("F-Droid"));
+ fetchMock.mockResolvedValue(
+ mockHtmlResponse("F-Droid"),
+ );
const card = renderGenericLinkPreview("https://fdroid.org/packages");
document.body.appendChild(card);
@@ -213,7 +229,9 @@ describe("renderGenericLinkPreview", () => {
it("allows previews for the configured OwnCord server even on private hosts", async () => {
setServerHost("LOCALHOST:8080");
- fetchMock.mockResolvedValue(mockHtmlResponse("OwnCord Local"));
+ fetchMock.mockResolvedValue(
+ mockHtmlResponse("OwnCord Local"),
+ );
const card = renderGenericLinkPreview("https://localhost:8080/docs");
document.body.appendChild(card);
@@ -298,7 +316,9 @@ describe("renderGenericLinkPreview", () => {
it("renders from cache on second call (no second fetch)", async () => {
clearEmbedCaches();
fetchMock.mockResolvedValueOnce(
- mockHtmlResponse(''),
+ mockHtmlResponse(
+ '',
+ ),
);
const card1 = renderGenericLinkPreview("https://cached.example.com/page");
@@ -333,7 +353,9 @@ describe("renderGenericLinkPreview", () => {
it("allows 172.32.x.x (not private)", async () => {
clearEmbedCaches();
- fetchMock.mockResolvedValueOnce(mockHtmlResponse("Public"));
+ fetchMock.mockResolvedValueOnce(
+ mockHtmlResponse("Public"),
+ );
const card = renderGenericLinkPreview("https://172.32.0.1/page");
document.body.appendChild(card);
await vi.waitFor(() => {
@@ -415,7 +437,9 @@ describe("renderGenericLinkPreview", () => {
it("allows public IPv6 addresses", async () => {
clearEmbedCaches();
- fetchMock.mockResolvedValueOnce(mockHtmlResponse("IPv6"));
+ fetchMock.mockResolvedValueOnce(
+ mockHtmlResponse("IPv6"),
+ );
const card = renderGenericLinkPreview("https://[2600::1]/page");
document.body.appendChild(card);
await vi.waitFor(() => {
@@ -495,7 +519,15 @@ describe("applyOgMeta", () => {
const imageWrap = document.createElement("div");
const meta: OgMeta = { title: "Page Title", description: null, image: null, siteName: null };
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(titleEl.textContent).toBe("Page Title");
});
@@ -507,7 +539,15 @@ describe("applyOgMeta", () => {
const imageWrap = document.createElement("div");
const meta: OgMeta = { title: null, description: null, image: null, siteName: null };
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(titleEl.textContent).toBe("example.com");
});
@@ -519,7 +559,15 @@ describe("applyOgMeta", () => {
const imageWrap = document.createElement("div");
const meta: OgMeta = { title: "Title", description: null, image: null, siteName: "My Site" };
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(hostEl.textContent).toBe("My Site");
});
@@ -532,7 +580,15 @@ describe("applyOgMeta", () => {
const longDesc = "A".repeat(300);
const meta: OgMeta = { title: "Title", description: longDesc, image: null, siteName: null };
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(descEl.textContent!.length).toBe(200);
expect(descEl.textContent!.endsWith("...")).toBe(true);
@@ -545,8 +601,21 @@ describe("applyOgMeta", () => {
const hostEl = document.createElement("div");
const imageWrap = document.createElement("div");
- const meta: OgMeta = { title: "Title", description: "Short description", image: null, siteName: null };
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ const meta: OgMeta = {
+ title: "Title",
+ description: "Short description",
+ image: null,
+ siteName: null,
+ };
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(descEl.textContent).toBe("Short description");
expect(descEl.style.display).toBe("");
@@ -559,7 +628,15 @@ describe("applyOgMeta", () => {
const imageWrap = document.createElement("div");
const meta: OgMeta = { title: "Title", description: null, image: null, siteName: null };
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(descEl.style.display).toBe("none");
});
@@ -576,7 +653,15 @@ describe("applyOgMeta", () => {
image: "https://example.com/image.jpg",
siteName: null,
};
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
const img = imageWrap.querySelector("img");
expect(img).not.toBeNull();
@@ -596,7 +681,15 @@ describe("applyOgMeta", () => {
image: "/images/og.png",
siteName: null,
};
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
const img = imageWrap.querySelector("img");
expect(img).not.toBeNull();
@@ -615,7 +708,15 @@ describe("applyOgMeta", () => {
image: "https://example.com/image.jpg",
siteName: null,
};
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
const img = imageWrap.querySelector("img")!;
img.dispatchEvent(new Event("error"));
@@ -630,7 +731,15 @@ describe("applyOgMeta", () => {
const imageWrap = document.createElement("div");
const meta: OgMeta = { title: "Title", description: null, image: "", siteName: null };
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(imageWrap.querySelector("img")).toBeNull();
});
@@ -647,7 +756,15 @@ describe("applyOgMeta", () => {
image: "https://192.168.1.1/image.png",
siteName: null,
};
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
expect(imageWrap.querySelector("img")).toBeNull();
});
@@ -664,7 +781,15 @@ describe("applyOgMeta", () => {
image: "https://example.com/animated.gif",
siteName: null,
};
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
const img = imageWrap.querySelector("img");
expect(img).not.toBeNull();
@@ -683,7 +808,15 @@ describe("applyOgMeta", () => {
image: "https://example.com/animated.gif",
siteName: null,
};
- applyOgMeta(meta, titleEl, descEl, hostEl, imageWrap, "https://example.com/page", "example.com");
+ applyOgMeta(
+ meta,
+ titleEl,
+ descEl,
+ hostEl,
+ imageWrap,
+ "https://example.com/page",
+ "example.com",
+ );
const img = imageWrap.querySelector("img")!;
img.dispatchEvent(new Event("load"));
@@ -710,9 +843,12 @@ describe("renderGenericLinkPreview — cache stale during non-HTML response", ()
it("discards non-HTML response result when cache was cleared mid-flight", async () => {
let resolveFetch: ((value: unknown) => void) | null = null;
- fetchMock.mockImplementationOnce(() => new Promise((resolve) => {
- resolveFetch = resolve;
- }));
+ fetchMock.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
+ );
const card = renderGenericLinkPreview("https://stale-nonhtml.example.com/data");
document.body.appendChild(card);
@@ -724,7 +860,9 @@ describe("renderGenericLinkPreview — cache stale during non-HTML response", ()
// Resolve with non-HTML content type
(resolveFetch as any)?.({
ok: true,
- headers: { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null },
+ headers: {
+ get: (name: string) => (name.toLowerCase() === "content-type" ? "application/json" : null),
+ },
text: vi.fn().mockResolvedValue("{}"),
});
@@ -733,7 +871,9 @@ describe("renderGenericLinkPreview — cache stale during non-HTML response", ()
// Should have discarded the result (generation mismatch)
// A new fetch for the same URL should still trigger a new fetch
- fetchMock.mockResolvedValueOnce(mockHtmlResponse("Fresh"));
+ fetchMock.mockResolvedValueOnce(
+ mockHtmlResponse("Fresh"),
+ );
const card2 = renderGenericLinkPreview("https://stale-nonhtml.example.com/data");
document.body.appendChild(card2);
await vi.waitFor(() => {
@@ -743,9 +883,12 @@ describe("renderGenericLinkPreview — cache stale during non-HTML response", ()
it("discards fetch error result when cache was cleared mid-flight", async () => {
let rejectFetch: ((err: Error) => void) | null = null;
- fetchMock.mockImplementationOnce(() => new Promise((_resolve, reject) => {
- rejectFetch = reject;
- }));
+ fetchMock.mockImplementationOnce(
+ () =>
+ new Promise((_resolve, reject) => {
+ rejectFetch = reject;
+ }),
+ );
const card = renderGenericLinkPreview("https://stale-error.example.com/fail");
document.body.appendChild(card);
@@ -761,11 +904,13 @@ describe("renderGenericLinkPreview — cache stale during non-HTML response", ()
await Promise.resolve();
// A new fetch for the same URL should still trigger a new fetch
- fetchMock.mockResolvedValueOnce(mockHtmlResponse("Recovered"));
+ fetchMock.mockResolvedValueOnce(
+ mockHtmlResponse("Recovered"),
+ );
const card2 = renderGenericLinkPreview("https://stale-error.example.com/fail");
document.body.appendChild(card2);
await vi.waitFor(() => {
expect(fetchMock).toHaveBeenCalledTimes(2);
});
});
-});
\ No newline at end of file
+});
diff --git a/Client/tauri-client/tests/unit/emoji-picker.test.ts b/Client/tauri-client/tests/unit/emoji-picker.test.ts
index a00f992c..53a78747 100644
--- a/Client/tauri-client/tests/unit/emoji-picker.test.ts
+++ b/Client/tauri-client/tests/unit/emoji-picker.test.ts
@@ -118,9 +118,7 @@ describe("EmojiPicker", () => {
it("renders custom emoji when provided", () => {
const { picker } = makePicker({
- customEmoji: [
- { shortcode: "test_emoji", url: "https://example.com/emoji.png" },
- ],
+ customEmoji: [{ shortcode: "test_emoji", url: "https://example.com/emoji.png" }],
});
const labels = picker.element.querySelectorAll(".ep-category-label");
diff --git a/Client/tauri-client/tests/unit/file-upload.test.ts b/Client/tauri-client/tests/unit/file-upload.test.ts
index 19f697bc..86769c99 100644
--- a/Client/tauri-client/tests/unit/file-upload.test.ts
+++ b/Client/tauri-client/tests/unit/file-upload.test.ts
@@ -126,7 +126,7 @@ describe("FileUpload", () => {
const onUpload = vi.fn(async () => {});
const upload = makeUpload({ onUpload, maxSizeMb: 5 });
- const bigFile = new File(["x"], "huge.bin", { type: "application/octet-stream" });
+ const bigFile = new File(["x"], "huge.pdf", { type: "application/pdf" });
Object.defineProperty(bigFile, "size", { value: 6 * 1024 * 1024 }); // 6 MB > 5 MB limit
const input = container.querySelector(".file-upload__input") as HTMLInputElement;
@@ -156,7 +156,7 @@ describe("FileUpload", () => {
const onUpload = vi.fn(async () => {});
const upload = makeUpload({ onUpload });
- const bigFile = new File(["x"], "huge.bin", { type: "application/octet-stream" });
+ const bigFile = new File(["x"], "huge.pdf", { type: "application/pdf" });
Object.defineProperty(bigFile, "size", { value: 11 * 1024 * 1024 }); // 11 MB > 10 MB default
const input = container.querySelector(".file-upload__input") as HTMLInputElement;
@@ -288,9 +288,12 @@ describe("FileUpload", () => {
it("cancel button aborts in-flight upload and resets preview", async () => {
let resolveUpload: (() => void) | undefined;
- const onUpload = vi.fn(() => new Promise((resolve) => {
- resolveUpload = resolve;
- }));
+ const onUpload = vi.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
const upload = makeUpload({ onUpload });
const file = new File(["data"], "test.txt", { type: "text/plain" });
@@ -327,7 +330,9 @@ describe("FileUpload", () => {
const root = container.querySelector(".file-upload") as HTMLDivElement;
const file = new File(["dropped"], "dropped.pdf", { type: "application/pdf" });
- const dropEvent = new Event("drop", { bubbles: true }) as Event & { dataTransfer?: { files: File[] } };
+ const dropEvent = new Event("drop", { bubbles: true }) as Event & {
+ dataTransfer?: { files: File[] };
+ };
Object.defineProperty(dropEvent, "dataTransfer", {
value: { files: [file] },
});
@@ -404,9 +409,12 @@ describe("FileUpload", () => {
it("destroy aborts in-flight upload", async () => {
let resolveUpload: (() => void) | undefined;
- const onUpload = vi.fn(() => new Promise((resolve) => {
- resolveUpload = resolve;
- }));
+ const onUpload = vi.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
const upload = makeUpload({ onUpload });
const file = new File(["data"], "test.txt", { type: "text/plain" });
@@ -448,7 +456,7 @@ describe("FileUpload", () => {
const onUpload = vi.fn(async () => {});
const upload = makeUpload({ onUpload, maxSizeMb: 20 });
- const file = new File(["x"], "big.bin", { type: "application/octet-stream" });
+ const file = new File(["x"], "big.pdf", { type: "application/pdf" });
Object.defineProperty(file, "size", { value: 5 * 1024 * 1024 }); // 5 MB
const input = container.querySelector(".file-upload__input") as HTMLInputElement;
diff --git a/Client/tauri-client/tests/unit/gif-picker.test.ts b/Client/tauri-client/tests/unit/gif-picker.test.ts
index 1f554371..1c24349d 100644
--- a/Client/tauri-client/tests/unit/gif-picker.test.ts
+++ b/Client/tauri-client/tests/unit/gif-picker.test.ts
@@ -28,16 +28,9 @@ function makeGif(id: string): TenorGif {
};
}
-const TRENDING_GIFS: readonly TenorGif[] = [
- makeGif("t1"),
- makeGif("t2"),
- makeGif("t3"),
-];
+const TRENDING_GIFS: readonly TenorGif[] = [makeGif("t1"), makeGif("t2"), makeGif("t3")];
-const SEARCH_GIFS: readonly TenorGif[] = [
- makeGif("s1"),
- makeGif("s2"),
-];
+const SEARCH_GIFS: readonly TenorGif[] = [makeGif("s1"), makeGif("s2")];
// ---------------------------------------------------------------------------
// Helpers
@@ -377,9 +370,7 @@ describe("GifPicker", () => {
const { picker } = makePicker({ onClose });
container.appendChild(picker.element);
- picker.element.dispatchEvent(
- new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
- );
+ picker.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
expect(onClose).toHaveBeenCalledOnce();
picker.destroy();
@@ -390,12 +381,8 @@ describe("GifPicker", () => {
const { picker } = makePicker({ onClose });
container.appendChild(picker.element);
- picker.element.dispatchEvent(
- new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
- );
- picker.element.dispatchEvent(
- new KeyboardEvent("keydown", { key: "Tab", bubbles: true }),
- );
+ picker.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
+ picker.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true }));
expect(onClose).not.toHaveBeenCalled();
picker.destroy();
@@ -549,8 +536,8 @@ describe("GifPicker", () => {
vi.mocked(getTrendingGifs).mockResolvedValue(TRENDING_GIFS);
vi.mocked(searchGifs)
- .mockReturnValueOnce(firstPromise) // "ca" — resolves late
- .mockResolvedValueOnce(SEARCH_GIFS); // "cats" — resolves immediately
+ .mockReturnValueOnce(firstPromise) // "ca" — resolves late
+ .mockResolvedValueOnce(SEARCH_GIFS); // "cats" — resolves immediately
const { picker } = makePicker();
container.appendChild(picker.element);
@@ -631,9 +618,7 @@ describe("GifPicker", () => {
picker.destroy();
- picker.element.dispatchEvent(
- new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
- );
+ picker.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
expect(onClose).not.toHaveBeenCalled();
});
diff --git a/Client/tauri-client/tests/unit/icons.test.ts b/Client/tauri-client/tests/unit/icons.test.ts
index 6b38d03d..92793672 100644
--- a/Client/tauri-client/tests/unit/icons.test.ts
+++ b/Client/tauri-client/tests/unit/icons.test.ts
@@ -110,7 +110,7 @@ describe("createIcon", () => {
const svg = createIcon(name);
expect(
svg.innerHTML.trim().length,
- `Expected non-empty innerHTML for icon "${name}"`
+ `Expected non-empty innerHTML for icon "${name}"`,
).toBeGreaterThan(0);
}
});
diff --git a/Client/tauri-client/tests/unit/keybinds-tab.test.ts b/Client/tauri-client/tests/unit/keybinds-tab.test.ts
index 417bf914..3315ad28 100644
--- a/Client/tauri-client/tests/unit/keybinds-tab.test.ts
+++ b/Client/tauri-client/tests/unit/keybinds-tab.test.ts
@@ -81,7 +81,9 @@ describe("KeybindsTab", () => {
it("shows 'Press any key...' when capture button is clicked", () => {
const el = buildKeybindsTab(new AbortController().signal);
mockCaptureKeyPress.mockReturnValue(new Promise(() => {})); // never resolves
- const pttBtn = el.querySelectorAll(".keybind-row")[0]!.querySelector(".kbd") as HTMLButtonElement;
+ const pttBtn = el
+ .querySelectorAll(".keybind-row")[0]!
+ .querySelector(".kbd") as HTMLButtonElement;
pttBtn.click();
@@ -94,7 +96,9 @@ describe("KeybindsTab", () => {
const el = buildKeybindsTab(new AbortController().signal);
mockCaptureKeyPress.mockResolvedValue(0x05); // Mouse 5
mockVkName.mockReturnValue("Mouse 5");
- const pttBtn = el.querySelectorAll(".keybind-row")[0]!.querySelector(".kbd") as HTMLButtonElement;
+ const pttBtn = el
+ .querySelectorAll(".keybind-row")[0]!
+ .querySelector(".kbd") as HTMLButtonElement;
pttBtn.click();
@@ -109,7 +113,9 @@ describe("KeybindsTab", () => {
it("restores previous value when captureKeyPress times out (returns 0)", async () => {
const el = buildKeybindsTab(new AbortController().signal);
mockCaptureKeyPress.mockResolvedValue(0);
- const pttBtn = el.querySelectorAll(".keybind-row")[0]!.querySelector(".kbd") as HTMLButtonElement;
+ const pttBtn = el
+ .querySelectorAll(".keybind-row")[0]!
+ .querySelector(".kbd") as HTMLButtonElement;
pttBtn.click();
@@ -122,7 +128,9 @@ describe("KeybindsTab", () => {
it("restores previous value on captureKeyPress failure (fallback path)", async () => {
const el = buildKeybindsTab(new AbortController().signal);
mockCaptureKeyPress.mockRejectedValue(new Error("No Tauri"));
- const pttBtn = el.querySelectorAll(".keybind-row")[0]!.querySelector(".kbd") as HTMLButtonElement;
+ const pttBtn = el
+ .querySelectorAll(".keybind-row")[0]!
+ .querySelector(".kbd") as HTMLButtonElement;
pttBtn.click();
@@ -136,7 +144,9 @@ describe("KeybindsTab", () => {
it("ignores click when already capturing", () => {
const el = buildKeybindsTab(new AbortController().signal);
mockCaptureKeyPress.mockReturnValue(new Promise(() => {})); // never resolves
- const pttBtn = el.querySelectorAll(".keybind-row")[0]!.querySelector(".kbd") as HTMLButtonElement;
+ const pttBtn = el
+ .querySelectorAll(".keybind-row")[0]!
+ .querySelector(".kbd") as HTMLButtonElement;
pttBtn.click();
expect(pttBtn.textContent).toBe("Press any key...");
@@ -247,7 +257,9 @@ describe("KeybindsTab", () => {
mockCaptureKeyPress.mockResolvedValue(0);
const el = buildKeybindsTab(new AbortController().signal);
- const pttBtn = el.querySelectorAll(".keybind-row")[0]!.querySelector(".kbd") as HTMLButtonElement;
+ const pttBtn = el
+ .querySelectorAll(".keybind-row")[0]!
+ .querySelector(".kbd") as HTMLButtonElement;
expect(pttBtn.textContent).toBe("F2");
@@ -266,7 +278,9 @@ describe("KeybindsTab", () => {
mockCaptureKeyPress.mockRejectedValue(new Error("No Tauri"));
const el = buildKeybindsTab(new AbortController().signal);
- const pttBtn = el.querySelectorAll(".keybind-row")[0]!.querySelector(".kbd") as HTMLButtonElement;
+ const pttBtn = el
+ .querySelectorAll(".keybind-row")[0]!
+ .querySelector(".kbd") as HTMLButtonElement;
expect(pttBtn.textContent).toBe("F2");
diff --git a/Client/tauri-client/tests/unit/log-persistence.test.ts b/Client/tauri-client/tests/unit/log-persistence.test.ts
index 3606e84d..cfaf0b86 100644
--- a/Client/tauri-client/tests/unit/log-persistence.test.ts
+++ b/Client/tauri-client/tests/unit/log-persistence.test.ts
@@ -64,12 +64,10 @@ function captureListener(): {
getListener: () => ((entry: unknown) => void) | null;
} {
let listener: ((entry: unknown) => void) | null = null;
- mockAddLogListener.mockImplementation(
- (cb: (entry: unknown) => void) => {
- listener = cb;
- return () => {};
- },
- );
+ mockAddLogListener.mockImplementation((cb: (entry: unknown) => void) => {
+ listener = cb;
+ return () => {};
+ });
return {
getListener: () => listener,
};
@@ -129,10 +127,7 @@ describe("log persistence", () => {
captureListener();
await initLogPersistence();
- expect(mockMkdir).toHaveBeenCalledWith(
- "/mock/logs/client-logs",
- { recursive: true },
- );
+ expect(mockMkdir).toHaveBeenCalledWith("/mock/logs/client-logs", { recursive: true });
});
it("does NOT create the log directory when it already exists", async () => {
@@ -380,8 +375,7 @@ describe("log persistence", () => {
describe("clearPendingPersistedLogs", () => {
it("clears the buffer so pending entries are discarded", async () => {
const { getListener } = captureListener();
- const { initLogPersistence, clearPendingPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, clearPendingPersistedLogs } = await freshImport();
await initLogPersistence();
getListener()!(makeEntry({ message: "will-be-cleared" }));
@@ -395,8 +389,7 @@ describe("log persistence", () => {
it("clears a pending flush timer", async () => {
const { getListener } = captureListener();
- const { initLogPersistence, clearPendingPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, clearPendingPersistedLogs } = await freshImport();
await initLogPersistence();
getListener()!(makeEntry());
@@ -418,8 +411,7 @@ describe("log persistence", () => {
}),
);
- const { initLogPersistence, clearPendingPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, clearPendingPersistedLogs } = await freshImport();
await initLogPersistence();
getListener()!(makeEntry());
@@ -487,12 +479,8 @@ describe("log persistence", () => {
// Should have removed the oldest files (7 files, keep 5 => remove 2)
expect(mockRemove).toHaveBeenCalledTimes(2);
- expect(mockRemove).toHaveBeenCalledWith(
- "/mock/logs/client-logs/2025-06-10.jsonl",
- );
- expect(mockRemove).toHaveBeenCalledWith(
- "/mock/logs/client-logs/2025-06-11.jsonl",
- );
+ expect(mockRemove).toHaveBeenCalledWith("/mock/logs/client-logs/2025-06-10.jsonl");
+ expect(mockRemove).toHaveBeenCalledWith("/mock/logs/client-logs/2025-06-11.jsonl");
});
it("does not rotate when file count is within MAX_LOG_FILES", async () => {
@@ -584,9 +572,7 @@ describe("log persistence", () => {
// 6 jsonl files - keep 5 = remove 1
expect(mockRemove).toHaveBeenCalledTimes(1);
- expect(mockRemove).toHaveBeenCalledWith(
- "/mock/logs/client-logs/2025-06-10.jsonl",
- );
+ expect(mockRemove).toHaveBeenCalledWith("/mock/logs/client-logs/2025-06-10.jsonl");
});
});
@@ -602,8 +588,7 @@ describe("log persistence", () => {
it("reads and concatenates all jsonl files in sorted order", async () => {
captureListener();
- const { initLogPersistence, readAllPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, readAllPersistedLogs } = await freshImport();
await initLogPersistence();
mockReadDir.mockResolvedValueOnce([
@@ -630,8 +615,7 @@ describe("log persistence", () => {
it("filters out directories and non-jsonl entries", async () => {
captureListener();
- const { initLogPersistence, readAllPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, readAllPersistedLogs } = await freshImport();
await initLogPersistence();
mockReadDir.mockResolvedValueOnce([
@@ -650,13 +634,10 @@ describe("log persistence", () => {
it("returns empty string when directory has no jsonl files", async () => {
captureListener();
- const { initLogPersistence, readAllPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, readAllPersistedLogs } = await freshImport();
await initLogPersistence();
- mockReadDir.mockResolvedValueOnce([
- { name: "notes.txt", isDirectory: false },
- ]);
+ mockReadDir.mockResolvedValueOnce([{ name: "notes.txt", isDirectory: false }]);
const result = await readAllPersistedLogs();
expect(result).toBe("");
@@ -665,8 +646,7 @@ describe("log persistence", () => {
it("returns empty string on readDir failure", async () => {
captureListener();
- const { initLogPersistence, readAllPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, readAllPersistedLogs } = await freshImport();
await initLogPersistence();
mockReadDir.mockRejectedValueOnce(new Error("no access"));
@@ -677,13 +657,10 @@ describe("log persistence", () => {
it("returns empty string on readTextFile failure", async () => {
captureListener();
- const { initLogPersistence, readAllPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, readAllPersistedLogs } = await freshImport();
await initLogPersistence();
- mockReadDir.mockResolvedValueOnce([
- { name: "2025-06-15.jsonl", isDirectory: false },
- ]);
+ mockReadDir.mockResolvedValueOnce([{ name: "2025-06-15.jsonl", isDirectory: false }]);
mockReadTextFile.mockRejectedValueOnce(new Error("corrupt file"));
const result = await readAllPersistedLogs();
@@ -735,8 +712,7 @@ describe("log persistence", () => {
describe("activeFlush tracking", () => {
it("clears activeFlush after successful flush", async () => {
const { getListener } = captureListener();
- const { initLogPersistence, clearPendingPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, clearPendingPersistedLogs } = await freshImport();
await initLogPersistence();
getListener()!(makeEntry());
@@ -751,8 +727,7 @@ describe("log persistence", () => {
it("clears activeFlush after failed flush", async () => {
mockWriteTextFile.mockRejectedValueOnce(new Error("write error"));
const { getListener } = captureListener();
- const { initLogPersistence, clearPendingPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, clearPendingPersistedLogs } = await freshImport();
await initLogPersistence();
getListener()!(makeEntry());
@@ -807,8 +782,7 @@ describe("log persistence", () => {
it("handles entries with undefined name in readAllPersistedLogs", async () => {
captureListener();
- const { initLogPersistence, readAllPersistedLogs } =
- await freshImport();
+ const { initLogPersistence, readAllPersistedLogs } = await freshImport();
await initLogPersistence();
mockReadDir.mockResolvedValueOnce([
@@ -849,9 +823,7 @@ describe("log persistence", () => {
getListener()!(makeEntry());
await vi.advanceTimersByTimeAsync(2000);
- expect(mockWriteTextFile.mock.calls[0]![0]).toBe(
- "/mock/logs/client-logs/2024-01-01.jsonl",
- );
+ expect(mockWriteTextFile.mock.calls[0]![0]).toBe("/mock/logs/client-logs/2024-01-01.jsonl");
});
it("cleanup final flush catches and logs errors", async () => {
diff --git a/Client/tauri-client/tests/unit/logger.test.ts b/Client/tauri-client/tests/unit/logger.test.ts
index 720257af..f5b18370 100644
--- a/Client/tauri-client/tests/unit/logger.test.ts
+++ b/Client/tauri-client/tests/unit/logger.test.ts
@@ -61,11 +61,7 @@ describe("logger", () => {
const log = createLogger("test");
log.info("with data", { key: "value" });
- expect(infoSpy).toHaveBeenCalledWith(
- expect.any(String),
- "with data",
- { key: "value" },
- );
+ expect(infoSpy).toHaveBeenCalledWith(expect.any(String), "with data", { key: "value" });
});
it("notifies listeners", () => {
@@ -111,9 +107,7 @@ describe("logger", () => {
expect(listener).toHaveBeenCalledTimes(1);
const entry = listener.mock.calls[0]?.[0];
- expect(entry.data).toEqual(
- expect.objectContaining({ error: "something broke" }),
- );
+ expect(entry.data).toEqual(expect.objectContaining({ error: "something broke" }));
expect(entry.data).toHaveProperty("stack");
unsub();
@@ -129,9 +123,7 @@ describe("logger", () => {
log.warn("context", { reason: err, count: 3 });
const entry = listener.mock.calls[0]?.[0];
- expect(entry.data.reason).toEqual(
- expect.objectContaining({ error: "inner error" }),
- );
+ expect(entry.data.reason).toEqual(expect.objectContaining({ error: "inner error" }));
expect(entry.data.count).toBe(3);
unsub();
@@ -168,10 +160,6 @@ describe("logger", () => {
log.info("no data");
// The third argument should be "" (empty string fallback)
- expect(infoSpy).toHaveBeenCalledWith(
- expect.any(String),
- "no data",
- "",
- );
+ expect(infoSpy).toHaveBeenCalledWith(expect.any(String), "no data", "");
});
});
diff --git a/Client/tauri-client/tests/unit/logs-tab.test.ts b/Client/tauri-client/tests/unit/logs-tab.test.ts
index b4f81662..0bbf3359 100644
--- a/Client/tauri-client/tests/unit/logs-tab.test.ts
+++ b/Client/tauri-client/tests/unit/logs-tab.test.ts
@@ -6,7 +6,7 @@ const {
mockClearLogBuffer,
mockAddLogListener,
mockSetLogLevel,
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
} = vi.hoisted(() => ({
mockGetLogBuffer: vi.fn(),
mockClearLogBuffer: vi.fn(),
@@ -325,7 +325,9 @@ describe("LogsTab", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockAddLogListener.mockImplementation(((cb: any) => {
logCallback = cb;
- return () => { logCallback = undefined; };
+ return () => {
+ logCallback = undefined;
+ };
}) as any);
const handle = createLogsTab(() => "Logs" as TabName, controller.signal);
@@ -345,7 +347,9 @@ describe("LogsTab", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockAddLogListener.mockImplementation(((cb: any) => {
logCallback = cb;
- return () => { logCallback = undefined; };
+ return () => {
+ logCallback = undefined;
+ };
}) as any);
const handle = createLogsTab(() => "Account" as TabName, controller.signal);
diff --git a/Client/tauri-client/tests/unit/media-cache.test.ts b/Client/tauri-client/tests/unit/media-cache.test.ts
index cd4479a7..cebb8e9d 100644
--- a/Client/tauri-client/tests/unit/media-cache.test.ts
+++ b/Client/tauri-client/tests/unit/media-cache.test.ts
@@ -38,9 +38,12 @@ describe("media cache clearing", () => {
it("replaces a stale loading title with a fallback when the cache is cleared mid-fetch", async () => {
let resolveFetch: ((value: ReturnType) => void) | undefined;
- fetchMock.mockImplementationOnce(() => new Promise((resolve) => {
- resolveFetch = resolve;
- }));
+ fetchMock.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
+ );
const element = renderYouTubeEmbed("abc123", "https://www.youtube.com/watch?v=abc123");
document.body.appendChild(element);
@@ -55,4 +58,4 @@ describe("media cache clearing", () => {
expect(title.textContent).toBe("YouTube Video");
});
});
-});
\ No newline at end of file
+});
diff --git a/Client/tauri-client/tests/unit/media-visibility.test.ts b/Client/tauri-client/tests/unit/media-visibility.test.ts
index dd3f48fb..b4430058 100644
--- a/Client/tauri-client/tests/unit/media-visibility.test.ts
+++ b/Client/tauri-client/tests/unit/media-visibility.test.ts
@@ -1,11 +1,11 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
observeMedia,
unobserveMedia,
pauseAllMedia,
resumeVisibleMedia,
destroyObserver,
-} from '../../src/lib/media-visibility';
+} from "../../src/lib/media-visibility";
// Mock IntersectionObserver
let observerCallback: IntersectionObserverCallback;
@@ -15,7 +15,7 @@ const disconnectMock = vi.fn();
class MockIntersectionObserver implements IntersectionObserver {
readonly root: Element | null = null;
- readonly rootMargin: string = '0px';
+ readonly rootMargin: string = "0px";
readonly thresholds: readonly number[] = [0];
constructor(callback: IntersectionObserverCallback) {
observerCallback = callback;
@@ -23,19 +23,21 @@ class MockIntersectionObserver implements IntersectionObserver {
observe = observeMock;
unobserve = unobserveMock;
disconnect = disconnectMock;
- takeRecords(): IntersectionObserverEntry[] { return []; }
+ takeRecords(): IntersectionObserverEntry[] {
+ return [];
+ }
}
function createFakeImg(src: string): HTMLImageElement {
- const img = document.createElement('img');
+ const img = document.createElement("img");
img.src = src;
- Object.defineProperty(img, 'naturalWidth', { value: 100 });
- Object.defineProperty(img, 'naturalHeight', { value: 100 });
+ Object.defineProperty(img, "naturalWidth", { value: 100 });
+ Object.defineProperty(img, "naturalHeight", { value: 100 });
return img;
}
function createWrapper(): HTMLDivElement {
- return document.createElement('div');
+ return document.createElement("div");
}
function fireIntersection(entries: Array<{ target: Element; isIntersecting: boolean }>): void {
@@ -52,20 +54,20 @@ function fireIntersection(entries: Array<{ target: Element; isIntersecting: bool
}
function setupCanvasMocks(): () => void {
- const mockCanvas = document.createElement('canvas');
+ const mockCanvas = document.createElement("canvas");
const mockCtx = { drawImage: vi.fn() };
const origCreateElement = document.createElement.bind(document);
- vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
- if (tag === 'canvas') return mockCanvas;
+ vi.spyOn(document, "createElement").mockImplementation((tag: string) => {
+ if (tag === "canvas") return mockCanvas;
return origCreateElement(tag);
});
- vi.spyOn(mockCanvas, 'getContext').mockReturnValue(mockCtx as any);
- vi.spyOn(mockCanvas, 'toDataURL').mockReturnValue('data:image/png;base64,frozen');
+ vi.spyOn(mockCanvas, "getContext").mockReturnValue(mockCtx as any);
+ vi.spyOn(mockCanvas, "toDataURL").mockReturnValue("data:image/png;base64,frozen");
return () => vi.restoreAllMocks();
}
beforeEach(() => {
- vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
+ vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
vi.useFakeTimers();
observeMock.mockClear();
unobserveMock.mockClear();
@@ -78,210 +80,210 @@ afterEach(() => {
vi.unstubAllGlobals();
});
-describe('media-visibility', () => {
- it('observeMedia registers image with IntersectionObserver', () => {
- const img = createFakeImg('https://example.com/cat.gif');
+describe("media-visibility", () => {
+ it("observeMedia registers image with IntersectionObserver", () => {
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
expect(observeMock).toHaveBeenCalledWith(img);
});
- it('adds play/pause button to wrapper', () => {
- const img = createFakeImg('https://example.com/cat.gif');
+ it("adds play/pause button to wrapper", () => {
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
- const btn = wrap.querySelector('.gif-play-btn');
+ observeMedia(img, "https://example.com/cat.gif", wrap);
+ const btn = wrap.querySelector(".gif-play-btn");
expect(btn).not.toBeNull();
});
- it('unobserveMedia stops observing and restores original src', () => {
- const img = createFakeImg('https://example.com/cat.gif');
+ it("unobserveMedia stops observing and restores original src", () => {
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
- img.src = 'data:image/png;base64,frozen';
+ observeMedia(img, "https://example.com/cat.gif", wrap);
+ img.src = "data:image/png;base64,frozen";
unobserveMedia(img);
expect(unobserveMock).toHaveBeenCalledWith(img);
- expect(img.src).toBe('https://example.com/cat.gif');
+ expect(img.src).toBe("https://example.com/cat.gif");
});
- it('does not double-observe same image', () => {
- const img = createFakeImg('https://example.com/cat.gif');
+ it("does not double-observe same image", () => {
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
- observeMedia(img, 'https://example.com/cat.gif', wrap);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
expect(observeMock).toHaveBeenCalledTimes(1);
});
- it('freezes GIF when it leaves viewport', () => {
+ it("freezes GIF when it leaves viewport", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/cat.gif');
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
fireIntersection([{ target: img, isIntersecting: false }]);
- expect(img.src).toBe('data:image/png;base64,frozen');
+ expect(img.src).toBe("data:image/png;base64,frozen");
cleanup();
});
- it('auto-pauses after 10 seconds', () => {
+ it("auto-pauses after 10 seconds", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/cat.gif');
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
- expect(img.src).toBe('https://example.com/cat.gif');
+ observeMedia(img, "https://example.com/cat.gif", wrap);
+ expect(img.src).toBe("https://example.com/cat.gif");
vi.advanceTimersByTime(10_000);
- expect(img.src).toBe('data:image/png;base64,frozen');
- const btn = wrap.querySelector('.gif-play-btn');
+ expect(img.src).toBe("data:image/png;base64,frozen");
+ const btn = wrap.querySelector(".gif-play-btn");
expect(btn?.querySelector('svg[data-icon="play"]')).not.toBeNull();
cleanup();
});
- it('play button click unfreezes and starts new 10s timer', () => {
+ it("play button click unfreezes and starts new 10s timer", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/cat.gif');
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
vi.advanceTimersByTime(10_000);
- expect(img.src).toBe('data:image/png;base64,frozen');
- const btn = wrap.querySelector('.gif-play-btn') as HTMLButtonElement;
+ expect(img.src).toBe("data:image/png;base64,frozen");
+ const btn = wrap.querySelector(".gif-play-btn") as HTMLButtonElement;
btn.click();
- expect(img.src).toBe('https://example.com/cat.gif');
+ expect(img.src).toBe("https://example.com/cat.gif");
vi.advanceTimersByTime(10_000);
- expect(img.src).toBe('data:image/png;base64,frozen');
+ expect(img.src).toBe("data:image/png;base64,frozen");
cleanup();
});
- it('pause button click freezes immediately', () => {
+ it("pause button click freezes immediately", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/cat.gif');
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
- expect(img.src).toBe('https://example.com/cat.gif');
- const btn = wrap.querySelector('.gif-play-btn') as HTMLButtonElement;
+ observeMedia(img, "https://example.com/cat.gif", wrap);
+ expect(img.src).toBe("https://example.com/cat.gif");
+ const btn = wrap.querySelector(".gif-play-btn") as HTMLButtonElement;
btn.click();
- expect(img.src).toBe('data:image/png;base64,frozen');
+ expect(img.src).toBe("data:image/png;base64,frozen");
cleanup();
});
- it('pauseAllMedia freezes all tracked GIFs', () => {
+ it("pauseAllMedia freezes all tracked GIFs", () => {
const cleanup = setupCanvasMocks();
- const img1 = createFakeImg('https://example.com/a.gif');
- const img2 = createFakeImg('https://example.com/b.gif');
+ const img1 = createFakeImg("https://example.com/a.gif");
+ const img2 = createFakeImg("https://example.com/b.gif");
const wrap1 = createWrapper();
const wrap2 = createWrapper();
- observeMedia(img1, 'https://example.com/a.gif', wrap1);
- observeMedia(img2, 'https://example.com/b.gif', wrap2);
+ observeMedia(img1, "https://example.com/a.gif", wrap1);
+ observeMedia(img2, "https://example.com/b.gif", wrap2);
pauseAllMedia();
- expect(img1.src).toBe('data:image/png;base64,frozen');
- expect(img2.src).toBe('data:image/png;base64,frozen');
+ expect(img1.src).toBe("data:image/png;base64,frozen");
+ expect(img2.src).toBe("data:image/png;base64,frozen");
cleanup();
});
- it('resumeVisibleMedia only unfreezes intersecting GIFs', () => {
- const img1 = createFakeImg('https://example.com/a.gif');
- const img2 = createFakeImg('https://example.com/b.gif');
+ it("resumeVisibleMedia only unfreezes intersecting GIFs", () => {
+ const img1 = createFakeImg("https://example.com/a.gif");
+ const img2 = createFakeImg("https://example.com/b.gif");
const wrap1 = createWrapper();
const wrap2 = createWrapper();
- observeMedia(img1, 'https://example.com/a.gif', wrap1);
- observeMedia(img2, 'https://example.com/b.gif', wrap2);
+ observeMedia(img1, "https://example.com/a.gif", wrap1);
+ observeMedia(img2, "https://example.com/b.gif", wrap2);
fireIntersection([
{ target: img1, isIntersecting: true },
{ target: img2, isIntersecting: false },
]);
- img1.src = 'data:image/png;base64,frozen';
- img2.src = 'data:image/png;base64,frozen';
+ img1.src = "data:image/png;base64,frozen";
+ img2.src = "data:image/png;base64,frozen";
resumeVisibleMedia();
- expect(img1.src).toBe('https://example.com/a.gif');
- expect(img2.src).toBe('data:image/png;base64,frozen');
+ expect(img1.src).toBe("https://example.com/a.gif");
+ expect(img2.src).toBe("data:image/png;base64,frozen");
});
- it('wrapper gets gif-paused class when frozen', () => {
+ it("wrapper gets gif-paused class when frozen", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/cat.gif');
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
- expect(wrap.classList.contains('gif-paused')).toBe(false);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
+ expect(wrap.classList.contains("gif-paused")).toBe(false);
vi.advanceTimersByTime(10_000);
- expect(wrap.classList.contains('gif-paused')).toBe(true);
+ expect(wrap.classList.contains("gif-paused")).toBe(true);
cleanup();
});
- it('destroyObserver cleans up', () => {
- const img = createFakeImg('https://example.com/cat.gif');
+ it("destroyObserver cleans up", () => {
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
destroyObserver();
expect(disconnectMock).toHaveBeenCalled();
});
- it('unobserveMedia does not start a dangling auto-timer', () => {
- const img = createFakeImg('https://example.com/cat.gif');
+ it("unobserveMedia does not start a dangling auto-timer", () => {
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap);
+ observeMedia(img, "https://example.com/cat.gif", wrap);
unobserveMedia(img);
const cleanup = setupCanvasMocks();
vi.advanceTimersByTime(15_000);
// toDataURL should NOT have been called (no dangling timer)
- const canvas = document.createElement('canvas');
+ const canvas = document.createElement("canvas");
expect(canvas.toDataURL).not.toHaveBeenCalled();
cleanup();
});
- it('starts frozen when startFrozen is true', () => {
+ it("starts frozen when startFrozen is true", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/cat.gif');
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap, true);
+ observeMedia(img, "https://example.com/cat.gif", wrap, true);
// Should be frozen immediately
- expect(wrap.classList.contains('gif-paused')).toBe(true);
+ expect(wrap.classList.contains("gif-paused")).toBe(true);
// The button should show play icon
- const btn = wrap.querySelector('.gif-play-btn');
+ const btn = wrap.querySelector(".gif-play-btn");
expect(btn?.querySelector('svg[data-icon="play"]')).not.toBeNull();
cleanup();
});
- it('startFrozen image can be unfrozen by clicking play', () => {
+ it("startFrozen image can be unfrozen by clicking play", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/cat.gif');
+ const img = createFakeImg("https://example.com/cat.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/cat.gif', wrap, true);
+ observeMedia(img, "https://example.com/cat.gif", wrap, true);
- expect(wrap.classList.contains('gif-paused')).toBe(true);
+ expect(wrap.classList.contains("gif-paused")).toBe(true);
// Click play to unfreeze
- const btn = wrap.querySelector('.gif-play-btn') as HTMLButtonElement;
+ const btn = wrap.querySelector(".gif-play-btn") as HTMLButtonElement;
btn.click();
- expect(img.src).toBe('https://example.com/cat.gif');
- expect(wrap.classList.contains('gif-paused')).toBe(false);
+ expect(img.src).toBe("https://example.com/cat.gif");
+ expect(wrap.classList.contains("gif-paused")).toBe(false);
cleanup();
});
- it('unobserveMedia on an untracked image is a no-op', () => {
- const img = createFakeImg('https://example.com/unknown.gif');
+ it("unobserveMedia on an untracked image is a no-op", () => {
+ const img = createFakeImg("https://example.com/unknown.gif");
// Should not throw
unobserveMedia(img);
expect(unobserveMock).not.toHaveBeenCalled();
});
- it('pauseAllMedia cleans up stale WeakRefs', () => {
+ it("pauseAllMedia cleans up stale WeakRefs", () => {
const cleanup = setupCanvasMocks();
- const img = createFakeImg('https://example.com/stale.gif');
+ const img = createFakeImg("https://example.com/stale.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/stale.gif', wrap);
+ observeMedia(img, "https://example.com/stale.gif", wrap);
// First, unobserve to remove from tracked but leave in allTracked
// Then pauseAllMedia should handle the missing entry gracefully
pauseAllMedia();
// Should not throw
- expect(img.src).toBe('data:image/png;base64,frozen');
+ expect(img.src).toBe("data:image/png;base64,frozen");
cleanup();
});
- it('resumeVisibleMedia cleans up stale WeakRefs without throwing', () => {
+ it("resumeVisibleMedia cleans up stale WeakRefs without throwing", () => {
// Register and immediately unobserve so the entry is gone from tracked
- const img = createFakeImg('https://example.com/gone.gif');
+ const img = createFakeImg("https://example.com/gone.gif");
const wrap = createWrapper();
- observeMedia(img, 'https://example.com/gone.gif', wrap);
+ observeMedia(img, "https://example.com/gone.gif", wrap);
unobserveMedia(img);
// Now resumeVisibleMedia should not crash
resumeVisibleMedia();
diff --git a/Client/tauri-client/tests/unit/media.test.ts b/Client/tauri-client/tests/unit/media.test.ts
index 6b770287..4582e6de 100644
--- a/Client/tauri-client/tests/unit/media.test.ts
+++ b/Client/tauri-client/tests/unit/media.test.ts
@@ -108,7 +108,10 @@ function fireImgError(parent: HTMLElement): void {
}
/** Create a MouseEvent with specified client coordinates. */
-function mouseEvent(type: string, opts: { clientX?: number; clientY?: number; deltaY?: number } = {}): MouseEvent {
+function mouseEvent(
+ type: string,
+ opts: { clientX?: number; clientY?: number; deltaY?: number } = {},
+): MouseEvent {
return new MouseEvent(type, {
bubbles: true,
cancelable: true,
@@ -204,12 +207,9 @@ describe("media.ts", () => {
// =========================================================================
describe("isDirectImageUrl", () => {
- it.each([".gif", ".png", ".jpg", ".jpeg", ".webp"])(
- "returns true for %s extension",
- (ext) => {
- expect(isDirectImageUrl(`https://example.com/photo${ext}`)).toBe(true);
- },
- );
+ it.each([".gif", ".png", ".jpg", ".jpeg", ".webp"])("returns true for %s extension", (ext) => {
+ expect(isDirectImageUrl(`https://example.com/photo${ext}`)).toBe(true);
+ });
it("returns true for uppercase extensions", () => {
expect(isDirectImageUrl("https://example.com/PHOTO.PNG")).toBe(true);
@@ -574,9 +574,7 @@ describe("media.ts", () => {
await vi.waitFor(() => {
// Last video should have its title
- const last = document.body.querySelector(
- `[href="https://www.youtube.com/watch?v=vid200"]`,
- );
+ const last = document.body.querySelector(`[href="https://www.youtube.com/watch?v=vid200"]`);
expect(last?.textContent).toBe("Title 200");
});
});
@@ -609,7 +607,10 @@ describe("media.ts", () => {
it("uses fallback title when cache generation changes during successful fetch", async () => {
let resolveFetch: ((value: ReturnType) => void) | null = null;
fetchMock.mockImplementationOnce(
- () => new Promise((resolve) => { resolveFetch = resolve; }),
+ () =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
);
const embed = renderYouTubeEmbed("gen1", "https://www.youtube.com/watch?v=gen1");
@@ -632,7 +633,10 @@ describe("media.ts", () => {
it("uses fallback title when cache generation changes during failed fetch", async () => {
let rejectFetch: ((reason: Error) => void) | null = null;
fetchMock.mockImplementationOnce(
- () => new Promise((_resolve, reject) => { rejectFetch = reject; }),
+ () =>
+ new Promise((_resolve, reject) => {
+ rejectFetch = reject;
+ }),
);
const embed = renderYouTubeEmbed("gen2", "https://www.youtube.com/watch?v=gen2");
@@ -811,18 +815,22 @@ describe("media.ts", () => {
});
// Simulate mousedown then click at same position (no drag)
- img.dispatchEvent(new MouseEvent("mousedown", {
- clientX: 200,
- clientY: 150,
- bubbles: true,
- cancelable: true,
- }));
- img.dispatchEvent(new MouseEvent("click", {
- clientX: 200,
- clientY: 150,
- bubbles: true,
- cancelable: true,
- }));
+ img.dispatchEvent(
+ new MouseEvent("mousedown", {
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ img.dispatchEvent(
+ new MouseEvent("click", {
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
// Should zoom to scale 3
const scaleMatch = img.style.transform.match(/scale\(([^)]+)\)/);
@@ -836,17 +844,42 @@ describe("media.ts", () => {
const img = document.body.querySelector(".image-lightbox img") as HTMLElement;
vi.spyOn(img, "getBoundingClientRect").mockReturnValue({
- left: 0, top: 0, width: 400, height: 300,
- right: 400, bottom: 300, x: 0, y: 0, toJSON: () => ({}),
+ left: 0,
+ top: 0,
+ width: 400,
+ height: 300,
+ right: 400,
+ bottom: 300,
+ x: 0,
+ y: 0,
+ toJSON: () => ({}),
});
// Zoom in first
- img.dispatchEvent(new MouseEvent("mousedown", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }));
- img.dispatchEvent(new MouseEvent("click", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }));
+ img.dispatchEvent(
+ new MouseEvent("mousedown", {
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ img.dispatchEvent(
+ new MouseEvent("click", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }),
+ );
// Now click again to zoom out (scale > 1.1 so it resets)
- img.dispatchEvent(new MouseEvent("mousedown", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }));
- img.dispatchEvent(new MouseEvent("click", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }));
+ img.dispatchEvent(
+ new MouseEvent("mousedown", {
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ img.dispatchEvent(
+ new MouseEvent("click", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }),
+ );
expect(img.style.transform).toContain("scale(1)");
});
@@ -857,8 +890,17 @@ describe("media.ts", () => {
const img = document.body.querySelector(".image-lightbox img") as HTMLElement;
// Mousedown at one position, click at another (moved > 5px)
- img.dispatchEvent(new MouseEvent("mousedown", { clientX: 100, clientY: 100, bubbles: true, cancelable: true }));
- img.dispatchEvent(new MouseEvent("click", { clientX: 120, clientY: 100, bubbles: true, cancelable: true }));
+ img.dispatchEvent(
+ new MouseEvent("mousedown", {
+ clientX: 100,
+ clientY: 100,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ img.dispatchEvent(
+ new MouseEvent("click", { clientX: 120, clientY: 100, bubbles: true, cancelable: true }),
+ );
// Should remain at scale(1) because dx=20 > 5
expect(img.style.transform).toBe("");
@@ -871,20 +913,45 @@ describe("media.ts", () => {
const overlay = document.body.querySelector(".image-lightbox") as HTMLElement;
vi.spyOn(img, "getBoundingClientRect").mockReturnValue({
- left: 0, top: 0, width: 400, height: 300,
- right: 400, bottom: 300, x: 0, y: 0, toJSON: () => ({}),
+ left: 0,
+ top: 0,
+ width: 400,
+ height: 300,
+ right: 400,
+ bottom: 300,
+ x: 0,
+ y: 0,
+ toJSON: () => ({}),
});
// Zoom in first
- img.dispatchEvent(new MouseEvent("mousedown", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }));
- img.dispatchEvent(new MouseEvent("click", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }));
+ img.dispatchEvent(
+ new MouseEvent("mousedown", {
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ img.dispatchEvent(
+ new MouseEvent("click", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }),
+ );
// Now start panning (mousedown while zoomed)
- img.dispatchEvent(new MouseEvent("mousedown", { clientX: 200, clientY: 150, bubbles: true, cancelable: true }));
+ img.dispatchEvent(
+ new MouseEvent("mousedown", {
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
expect(overlay.classList.contains("dragging")).toBe(true);
// Move mouse
- document.dispatchEvent(new MouseEvent("mousemove", { clientX: 250, clientY: 200, bubbles: true }));
+ document.dispatchEvent(
+ new MouseEvent("mousemove", { clientX: 250, clientY: 200, bubbles: true }),
+ );
// Transform should reflect pan offset
expect(img.style.transform).toContain("translate(");
@@ -908,7 +975,9 @@ describe("media.ts", () => {
const img = document.body.querySelector(".image-lightbox img") as HTMLElement;
// mousemove without prior drag should not alter transform
- document.dispatchEvent(new MouseEvent("mousemove", { clientX: 300, clientY: 300, bubbles: true }));
+ document.dispatchEvent(
+ new MouseEvent("mousemove", { clientX: 300, clientY: 300, bubbles: true }),
+ );
expect(img.style.transform).toBe("");
});
@@ -919,8 +988,15 @@ describe("media.ts", () => {
const imgWrap = document.body.querySelector(".image-lightbox-wrap") as HTMLElement;
vi.spyOn(img, "getBoundingClientRect").mockReturnValue({
- left: 0, top: 0, width: 400, height: 300,
- right: 400, bottom: 300, x: 0, y: 0, toJSON: () => ({}),
+ left: 0,
+ top: 0,
+ width: 400,
+ height: 300,
+ right: 400,
+ bottom: 300,
+ x: 0,
+ y: 0,
+ toJSON: () => ({}),
});
// Scroll up (negative deltaY = zoom in)
@@ -945,8 +1021,15 @@ describe("media.ts", () => {
const imgWrap = document.body.querySelector(".image-lightbox-wrap") as HTMLElement;
vi.spyOn(img, "getBoundingClientRect").mockReturnValue({
- left: 0, top: 0, width: 400, height: 300,
- right: 400, bottom: 300, x: 0, y: 0, toJSON: () => ({}),
+ left: 0,
+ top: 0,
+ width: 400,
+ height: 300,
+ right: 400,
+ bottom: 300,
+ x: 0,
+ y: 0,
+ toJSON: () => ({}),
});
// Scroll down (positive deltaY = zoom out)
@@ -971,15 +1054,28 @@ describe("media.ts", () => {
const imgWrap = document.body.querySelector(".image-lightbox-wrap") as HTMLElement;
vi.spyOn(img, "getBoundingClientRect").mockReturnValue({
- left: 0, top: 0, width: 400, height: 300,
- right: 400, bottom: 300, x: 0, y: 0, toJSON: () => ({}),
+ left: 0,
+ top: 0,
+ width: 400,
+ height: 300,
+ right: 400,
+ bottom: 300,
+ x: 0,
+ y: 0,
+ toJSON: () => ({}),
});
// Zoom out many times
for (let i = 0; i < 20; i++) {
- imgWrap.dispatchEvent(new WheelEvent("wheel", {
- deltaY: 100, clientX: 200, clientY: 150, bubbles: true, cancelable: true,
- }));
+ imgWrap.dispatchEvent(
+ new WheelEvent("wheel", {
+ deltaY: 100,
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
}
const scaleMatch = img.style.transform.match(/scale\(([^)]+)\)/);
@@ -993,15 +1089,28 @@ describe("media.ts", () => {
const imgWrap = document.body.querySelector(".image-lightbox-wrap") as HTMLElement;
vi.spyOn(img, "getBoundingClientRect").mockReturnValue({
- left: 0, top: 0, width: 400, height: 300,
- right: 400, bottom: 300, x: 0, y: 0, toJSON: () => ({}),
+ left: 0,
+ top: 0,
+ width: 400,
+ height: 300,
+ right: 400,
+ bottom: 300,
+ x: 0,
+ y: 0,
+ toJSON: () => ({}),
});
// Zoom in many times
for (let i = 0; i < 50; i++) {
- imgWrap.dispatchEvent(new WheelEvent("wheel", {
- deltaY: -100, clientX: 200, clientY: 150, bubbles: true, cancelable: true,
- }));
+ imgWrap.dispatchEvent(
+ new WheelEvent("wheel", {
+ deltaY: -100,
+ clientX: 200,
+ clientY: 150,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
}
const scaleMatch = img.style.transform.match(/scale\(([^)]+)\)/);
@@ -1053,7 +1162,9 @@ describe("media.ts", () => {
// After close, key events should not error or affect anything
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
- document.dispatchEvent(new MouseEvent("mousemove", { clientX: 100, clientY: 100, bubbles: true }));
+ document.dispatchEvent(
+ new MouseEvent("mousemove", { clientX: 100, clientY: 100, bubbles: true }),
+ );
document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
});
});
diff --git a/Client/tauri-client/tests/unit/member-list.test.ts b/Client/tauri-client/tests/unit/member-list.test.ts
index 9a958e68..248054c5 100644
--- a/Client/tauri-client/tests/unit/member-list.test.ts
+++ b/Client/tauri-client/tests/unit/member-list.test.ts
@@ -195,7 +195,9 @@ describe("MemberList", () => {
it("skips role groups that have no members", () => {
// Only add an owner — other groups should not render
- setTestMembers([makeMember({ id: 1, username: "Alice", role: "owner", status: "online" as UserStatus })]);
+ setTestMembers([
+ makeMember({ id: 1, username: "Alice", role: "owner", status: "online" as UserStatus }),
+ ]);
memberList.mount(container);
const headers = container.querySelectorAll(".member-role-group");
@@ -314,7 +316,9 @@ describe("MemberList", () => {
expect(container.querySelectorAll(".member-item").length).toBe(6);
// Remove all but one member
- setTestMembers([makeMember({ id: 99, username: "Solo", role: "member", status: "online" as UserStatus })]);
+ setTestMembers([
+ makeMember({ id: 99, username: "Solo", role: "member", status: "online" as UserStatus }),
+ ]);
membersStore.flush();
expect(container.querySelectorAll(".member-item").length).toBe(1);
diff --git a/Client/tauri-client/tests/unit/member-picker-modal.test.ts b/Client/tauri-client/tests/unit/member-picker-modal.test.ts
index 41b0acbf..3eb9ff1f 100644
--- a/Client/tauri-client/tests/unit/member-picker-modal.test.ts
+++ b/Client/tauri-client/tests/unit/member-picker-modal.test.ts
@@ -197,9 +197,7 @@ describe("createMemberPickerModal", () => {
it("shows empty list when only the current user exists", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 1, username: "Me" }),
- ]);
+ setStoreMembers([makeMember({ id: 1, username: "Me" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -233,9 +231,7 @@ describe("createMemberPickerModal", () => {
it("renders avatar with uppercased first letter of username", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "alice" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "alice" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -252,9 +248,7 @@ describe("createMemberPickerModal", () => {
it("renders username text in the item", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Bob" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Bob" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -272,9 +266,7 @@ describe("createMemberPickerModal", () => {
it("renders member status text", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Alice", status: "online" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Alice", status: "online" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -290,9 +282,7 @@ describe("createMemberPickerModal", () => {
it("renders offline status for offline members", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Alice", status: "offline" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Alice", status: "offline" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -308,9 +298,7 @@ describe("createMemberPickerModal", () => {
it("applies green color for online status", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Alice", status: "online" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Alice", status: "online" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -329,9 +317,7 @@ describe("createMemberPickerModal", () => {
it("applies micro text color for non-online status", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Alice", status: "idle" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Alice", status: "idle" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -351,9 +337,7 @@ describe("createMemberPickerModal", () => {
it("calls onSelect with the member's user ID when a member item is clicked", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 42, username: "Alice" }),
- ]);
+ setStoreMembers([makeMember({ id: 42, username: "Alice" })]);
const onSelect = vi.fn();
const component = createMemberPickerModal({
@@ -373,9 +357,7 @@ describe("createMemberPickerModal", () => {
it("closes the modal when a member is selected", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Alice" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Alice" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -604,9 +586,7 @@ describe("createMemberPickerModal", () => {
it("each member item has the channel-item class", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Alice" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Alice" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
@@ -622,9 +602,7 @@ describe("createMemberPickerModal", () => {
it("each member item has cursor pointer style", () => {
setCurrentUser(1);
- setStoreMembers([
- makeMember({ id: 2, username: "Alice" }),
- ]);
+ setStoreMembers([makeMember({ id: 2, username: "Alice" })]);
const component = createMemberPickerModal({
onSelect: vi.fn(),
diff --git a/Client/tauri-client/tests/unit/message-controller.test.ts b/Client/tauri-client/tests/unit/message-controller.test.ts
index 2e4035ea..6afb9ea9 100644
--- a/Client/tauri-client/tests/unit/message-controller.test.ts
+++ b/Client/tauri-client/tests/unit/message-controller.test.ts
@@ -80,11 +80,7 @@ describe("createMessageController", () => {
await ctrl.loadMessages(42, signal);
expect(api.getMessages).toHaveBeenCalledWith(42, { limit: 50 }, signal);
- expect(mockSetMessages).toHaveBeenCalledWith(
- 42,
- [{ id: 1, content: "hi" }],
- false,
- );
+ expect(mockSetMessages).toHaveBeenCalledWith(42, [{ id: 1, content: "hi" }], false);
});
it("skips fetch when channel is already loaded", async () => {
@@ -155,16 +151,8 @@ describe("createMessageController", () => {
await ctrl.loadOlderMessages(42, signal);
- expect(api.getMessages).toHaveBeenCalledWith(
- 42,
- { before: 10, limit: 50 },
- signal,
- );
- expect(mockPrependMessages).toHaveBeenCalledWith(
- 42,
- [{ id: 5, content: "older" }],
- true,
- );
+ expect(api.getMessages).toHaveBeenCalledWith(42, { before: 10, limit: 50 }, signal);
+ expect(mockPrependMessages).toHaveBeenCalledWith(42, [{ id: 5, content: "older" }], true);
});
it("does nothing when channel has no messages", async () => {
diff --git a/Client/tauri-client/tests/unit/message-input.test.ts b/Client/tauri-client/tests/unit/message-input.test.ts
index 5ef27650..db91e22a 100644
--- a/Client/tauri-client/tests/unit/message-input.test.ts
+++ b/Client/tauri-client/tests/unit/message-input.test.ts
@@ -1,7 +1,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
/** Captured emoji picker callbacks so tests can simulate selection. */
-let lastEmojiPickerOptions: { onSelect: (emoji: string) => void; onClose: () => void } | null = null;
+let lastEmojiPickerOptions: { onSelect: (emoji: string) => void; onClose: () => void } | null =
+ null;
vi.mock("@components/EmojiPicker", () => ({
createEmojiPicker: (opts: { onSelect: (emoji: string) => void; onClose: () => void }) => {
@@ -24,10 +25,7 @@ vi.mock("@components/GifPicker", () => ({
},
}));
-import {
- createMessageInput,
- type MessageInputOptions,
-} from "@components/MessageInput";
+import { createMessageInput, type MessageInputOptions } from "@components/MessageInput";
function makeOptions(overrides: Partial = {}): MessageInputOptions {
return {
@@ -100,9 +98,7 @@ describe("MessageInput", () => {
const textarea = container.querySelector(".msg-textarea") as HTMLTextAreaElement;
textarea.value = "Enter message";
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
- );
+ textarea.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
expect(opts.onSend).toHaveBeenCalledWith("Enter message", null, []);
@@ -333,9 +329,7 @@ describe("MessageInput", () => {
comp.startEdit(88, "editing");
const textarea = container.querySelector(".msg-textarea") as HTMLTextAreaElement;
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
- );
+ textarea.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
// Edit bar should be hidden
const bars = container.querySelectorAll(".reply-bar");
@@ -354,9 +348,7 @@ describe("MessageInput", () => {
comp.setReplyTo(44, "replyuser");
const textarea = container.querySelector(".msg-textarea") as HTMLTextAreaElement;
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
- );
+ textarea.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
const replyBar = container.querySelector(".reply-bar") as HTMLDivElement;
expect(replyBar.classList.contains("visible")).toBe(false);
@@ -377,9 +369,7 @@ describe("MessageInput", () => {
const listener = vi.fn();
container.addEventListener("edit-last-message", listener);
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }),
- );
+ textarea.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }));
expect(listener).toHaveBeenCalledTimes(1);
@@ -397,9 +387,7 @@ describe("MessageInput", () => {
const listener = vi.fn();
container.addEventListener("edit-last-message", listener);
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }),
- );
+ textarea.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }));
expect(listener).not.toHaveBeenCalled();
@@ -501,7 +489,7 @@ describe("MessageInput", () => {
expect(onUploadFile).not.toHaveBeenCalled();
const error = container.querySelector(".attachment-upload-error");
expect(error).not.toBeNull();
- expect(error!.textContent).toContain("Unsupported file type");
+ expect(error!.textContent).toContain("is not a supported file type");
comp.destroy?.();
});
@@ -532,9 +520,12 @@ describe("MessageInput", () => {
it("blocks send while uploads are still in flight", async () => {
// Create an upload that never resolves during the test
let resolveUpload: ((v: { id: string; url: string; filename: string }) => void) | null = null;
- const onUploadFile = vi.fn(() => new Promise<{ id: string; url: string; filename: string }>((res) => {
- resolveUpload = res;
- }));
+ const onUploadFile = vi.fn(
+ () =>
+ new Promise<{ id: string; url: string; filename: string }>((res) => {
+ resolveUpload = res;
+ }),
+ );
const opts = makeOptions({ onUploadFile });
const comp = createMessageInput(opts);
comp.mount(container);
@@ -567,9 +558,12 @@ describe("MessageInput", () => {
it("remove button removes attachment preview while upload is pending", async () => {
// Use a long-running upload so we can click remove while it's still pending
let resolveUpload: ((v: { id: string; url: string; filename: string }) => void) | null = null;
- const onUploadFile = vi.fn(() => new Promise<{ id: string; url: string; filename: string }>((res) => {
- resolveUpload = res;
- }));
+ const onUploadFile = vi.fn(
+ () =>
+ new Promise<{ id: string; url: string; filename: string }>((res) => {
+ resolveUpload = res;
+ }),
+ );
const opts = makeOptions({ onUploadFile });
const comp = createMessageInput(opts);
comp.mount(container);
@@ -822,7 +816,11 @@ describe("MessageInput", () => {
// ── Paste file handling ──
it("pasting an image file triggers upload", async () => {
- const onUploadFile = vi.fn(async () => ({ id: "paste-1", url: "http://x", filename: "paste.png" }));
+ const onUploadFile = vi.fn(async () => ({
+ id: "paste-1",
+ url: "http://x",
+ filename: "paste.png",
+ }));
const opts = makeOptions({ onUploadFile });
const comp = createMessageInput(opts);
comp.mount(container);
@@ -837,11 +835,13 @@ describe("MessageInput", () => {
};
Object.defineProperty(pasteEvent, "clipboardData", {
value: {
- items: [{
- kind: "file",
- type: "image/png",
- getAsFile: () => file,
- }],
+ items: [
+ {
+ kind: "file",
+ type: "image/png",
+ getAsFile: () => file,
+ },
+ ],
},
});
@@ -854,9 +854,9 @@ describe("MessageInput", () => {
comp.destroy?.();
});
- // ── Files with empty MIME type are accepted ──
+ // ── Files with empty MIME type are rejected (security hardening) ──
- it("files with empty MIME type bypass type validation", async () => {
+ it("files with empty MIME type are rejected", async () => {
const onUploadFile = vi.fn(async () => ({ id: "unk-1", url: "http://x", filename: "data" }));
const opts = makeOptions({ onUploadFile });
const comp = createMessageInput(opts);
@@ -867,9 +867,12 @@ describe("MessageInput", () => {
Object.defineProperty(fileInput, "files", { value: [noTypeFile], writable: true });
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
- await vi.waitFor(() => {
- expect(onUploadFile).toHaveBeenCalledWith(noTypeFile);
- });
+ await new Promise((r) => setTimeout(r, 10));
+
+ expect(onUploadFile).not.toHaveBeenCalled();
+ const error = container.querySelector(".attachment-upload-error");
+ expect(error).not.toBeNull();
+ expect(error!.textContent).toContain("is not a supported file type");
comp.destroy?.();
});
diff --git a/Client/tauri-client/tests/unit/message-list.test.ts b/Client/tauri-client/tests/unit/message-list.test.ts
index 67ea1853..aebd0434 100644
--- a/Client/tauri-client/tests/unit/message-list.test.ts
+++ b/Client/tauri-client/tests/unit/message-list.test.ts
@@ -3,9 +3,15 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// jsdom does not provide ResizeObserver — stub it so MessageList can mount.
if (typeof globalThis.ResizeObserver === "undefined") {
globalThis.ResizeObserver = class {
- observe(): void { /* noop */ }
- unobserve(): void { /* noop */ }
- disconnect(): void { /* noop */ }
+ observe(): void {
+ /* noop */
+ }
+ unobserve(): void {
+ /* noop */
+ }
+ disconnect(): void {
+ /* noop */
+ }
} as unknown as typeof ResizeObserver;
}
@@ -208,7 +214,9 @@ describe("MessageList", () => {
expect(icon?.textContent).toBe("@");
const text = container.querySelector(".channel-welcome-text");
- expect(text?.textContent).toBe("This is the beginning of your direct message history with Bob.");
+ expect(text?.textContent).toBe(
+ "This is the beginning of your direct message history with Bob.",
+ );
});
it("includes a scroll-to-bottom button", () => {
@@ -265,10 +273,7 @@ describe("MessageList", () => {
expect(options.onScrollTop).toHaveBeenCalledTimes(1);
// Simulate new messages arriving (load-more response)
- setMessages(1, [
- makeMessage({ id: 0, content: "Older message" }),
- makeMessage({ id: 1 }),
- ]);
+ setMessages(1, [makeMessage({ id: 0, content: "Older message" }), makeMessage({ id: 1 })]);
messagesStore.flush();
// Now scrolling to top again should trigger onScrollTop again
@@ -310,10 +315,7 @@ describe("MessageList", () => {
});
it("destroys cleanly without errors even with loaded messages", () => {
- setMessages(1, [
- makeMessage({ id: 1 }),
- makeMessage({ id: 2 }),
- ]);
+ setMessages(1, [makeMessage({ id: 1 }), makeMessage({ id: 2 })]);
msgList.mount(container);
expect(container.querySelector(".messages-container")).not.toBeNull();
diff --git a/Client/tauri-client/tests/unit/messages.store.test.ts b/Client/tauri-client/tests/unit/messages.store.test.ts
index b560e121..e0441e38 100644
--- a/Client/tauri-client/tests/unit/messages.store.test.ts
+++ b/Client/tauri-client/tests/unit/messages.store.test.ts
@@ -181,10 +181,7 @@ describe("messages store", () => {
describe("setMessages", () => {
it("sets messages for a channel", () => {
// API returns newest-first; store reverses to oldest-first for display.
- const responses = [
- makeMessageResponse({ id: 11 }),
- makeMessageResponse({ id: 10 }),
- ];
+ const responses = [makeMessageResponse({ id: 11 }), makeMessageResponse({ id: 10 })];
setMessages(1, responses, false);
const msgs = getChannelMessages(1);
@@ -234,11 +231,7 @@ describe("messages store", () => {
it("prepends older messages before existing ones", () => {
// API returns newest-first; store reverses to oldest-first.
setMessages(1, [makeMessageResponse({ id: 20 })], true);
- prependMessages(
- 1,
- [makeMessageResponse({ id: 15 }), makeMessageResponse({ id: 10 })],
- false,
- );
+ prependMessages(1, [makeMessageResponse({ id: 15 }), makeMessageResponse({ id: 10 })], false);
const msgs = getChannelMessages(1);
expect(msgs).toHaveLength(3);
@@ -565,13 +558,16 @@ describe("messages store", () => {
it("marks reaction as 'me' when current user reacts", () => {
addMessage(makeChatPayload({ id: 100, channel_id: 1 }));
- updateReaction({
- message_id: 100,
- channel_id: 1,
- emoji: "❤️",
- user_id: 1,
- action: "add",
- }, 1);
+ updateReaction(
+ {
+ message_id: 100,
+ channel_id: 1,
+ emoji: "❤️",
+ user_id: 1,
+ action: "add",
+ },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions[0]).toEqual({ emoji: "❤️", count: 1, me: true });
@@ -580,21 +576,27 @@ describe("messages store", () => {
it("increments count on existing reaction", () => {
addMessage(makeChatPayload({ id: 100, channel_id: 1 }));
- updateReaction({
- message_id: 100,
- channel_id: 1,
- emoji: "👍",
- user_id: 2,
- action: "add",
- }, 1);
+ updateReaction(
+ {
+ message_id: 100,
+ channel_id: 1,
+ emoji: "👍",
+ user_id: 2,
+ action: "add",
+ },
+ 1,
+ );
- updateReaction({
- message_id: 100,
- channel_id: 1,
- emoji: "👍",
- user_id: 3,
- action: "add",
- }, 1);
+ updateReaction(
+ {
+ message_id: 100,
+ channel_id: 1,
+ emoji: "👍",
+ user_id: 3,
+ action: "add",
+ },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions).toHaveLength(1);
@@ -604,21 +606,27 @@ describe("messages store", () => {
it("sets me=true when incrementing existing reaction by current user", () => {
addMessage(makeChatPayload({ id: 100, channel_id: 1 }));
- updateReaction({
- message_id: 100,
- channel_id: 1,
- emoji: "👍",
- user_id: 2,
- action: "add",
- }, 1);
+ updateReaction(
+ {
+ message_id: 100,
+ channel_id: 1,
+ emoji: "👍",
+ user_id: 2,
+ action: "add",
+ },
+ 1,
+ );
- updateReaction({
- message_id: 100,
- channel_id: 1,
- emoji: "👍",
- user_id: 1,
- action: "add",
- }, 1);
+ updateReaction(
+ {
+ message_id: 100,
+ channel_id: 1,
+ emoji: "👍",
+ user_id: 1,
+ action: "add",
+ },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions[0]!.me).toBe(true);
@@ -632,7 +640,10 @@ describe("messages store", () => {
updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 3, action: "add" }, 1);
// Remove one
- updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 3, action: "remove" }, 1);
+ updateReaction(
+ { message_id: 100, channel_id: 1, emoji: "👍", user_id: 3, action: "remove" },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions).toHaveLength(1);
@@ -643,7 +654,10 @@ describe("messages store", () => {
addMessage(makeChatPayload({ id: 100, channel_id: 1 }));
updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "add" }, 1);
- updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "remove" }, 1);
+ updateReaction(
+ { message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "remove" },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions).toHaveLength(0);
@@ -656,7 +670,10 @@ describe("messages store", () => {
updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "add" }, 1);
expect(getChannelMessages(1)[0]!.reactions[0]!.me).toBe(true);
- updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 1, action: "remove" }, 1);
+ updateReaction(
+ { message_id: 100, channel_id: 1, emoji: "👍", user_id: 1, action: "remove" },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions[0]!.me).toBe(false);
@@ -665,7 +682,10 @@ describe("messages store", () => {
it("is a no-op if the channel does not exist", () => {
const before = messagesStore.getState();
- updateReaction({ message_id: 999, channel_id: 99, emoji: "👍", user_id: 1, action: "add" }, 1);
+ updateReaction(
+ { message_id: 999, channel_id: 99, emoji: "👍", user_id: 1, action: "add" },
+ 1,
+ );
const after = messagesStore.getState();
expect(before).toBe(after);
});
@@ -686,7 +706,10 @@ describe("messages store", () => {
updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 1, action: "add" }, 1);
updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "add" }, 1);
- updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "remove" }, 1);
+ updateReaction(
+ { message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "remove" },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions[0]!.me).toBe(true);
@@ -719,7 +742,10 @@ describe("messages store", () => {
updateReaction({ message_id: 100, channel_id: 1, emoji: "❤️", user_id: 3, action: "add" }, 1);
// Remove 👍 — ❤️ should remain unchanged
- updateReaction({ message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "remove" }, 1);
+ updateReaction(
+ { message_id: 100, channel_id: 1, emoji: "👍", user_id: 2, action: "remove" },
+ 1,
+ );
const msg = getChannelMessages(1)[0]!;
expect(msg.reactions).toHaveLength(1);
diff --git a/Client/tauri-client/tests/unit/modal-factory.test.ts b/Client/tauri-client/tests/unit/modal-factory.test.ts
index 680f23ad..8ea171d4 100644
--- a/Client/tauri-client/tests/unit/modal-factory.test.ts
+++ b/Client/tauri-client/tests/unit/modal-factory.test.ts
@@ -38,10 +38,7 @@ describe("createModal", () => {
it("applies overlay attributes", () => {
const content = document.createElement("div");
- const inst = createModal(
- { content, overlayAttrs: { "data-testid": "my-modal" } },
- container,
- );
+ const inst = createModal({ content, overlayAttrs: { "data-testid": "my-modal" } }, container);
expect(inst.overlay.getAttribute("data-testid")).toBe("my-modal");
});
@@ -104,10 +101,7 @@ describe("createModal", () => {
it("closeOnBackdrop=false prevents backdrop close", () => {
const onClose = vi.fn();
const content = document.createElement("div");
- const inst = createModal(
- { content, onClose, closeOnBackdrop: false },
- container,
- );
+ const inst = createModal({ content, onClose, closeOnBackdrop: false }, container);
inst.overlay.dispatchEvent(new MouseEvent("click", { bubbles: true }));
@@ -118,10 +112,7 @@ describe("createModal", () => {
it("closeOnEscape=false prevents Escape close", () => {
const onClose = vi.fn();
const content = document.createElement("div");
- createModal(
- { content, onClose, closeOnEscape: false },
- container,
- );
+ createModal({ content, onClose, closeOnEscape: false }, container);
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
@@ -131,10 +122,7 @@ describe("createModal", () => {
it("cleans up when external signal is aborted", () => {
const externalAc = new AbortController();
const content = document.createElement("div");
- const inst = createModal(
- { content, signal: externalAc.signal },
- container,
- );
+ const inst = createModal({ content, signal: externalAc.signal }, container);
expect(container.contains(inst.overlay)).toBe(true);
diff --git a/Client/tauri-client/tests/unit/notifications.test.ts b/Client/tauri-client/tests/unit/notifications.test.ts
index 62ef7ae0..21e30881 100644
--- a/Client/tauri-client/tests/unit/notifications.test.ts
+++ b/Client/tauri-client/tests/unit/notifications.test.ts
@@ -84,8 +84,12 @@ const mockGain = {
class MockAudioContext {
readonly currentTime = 0;
readonly destination = {};
- createOscillator() { return mockOscillator; }
- createGain() { return mockGain; }
+ createOscillator() {
+ return mockOscillator;
+ }
+ createGain() {
+ return mockGain;
+ }
}
// Set up AudioContext mock globally
@@ -106,7 +110,20 @@ describe("notifyIncomingMessage", () => {
// Set up channels store
channelsStore.setState(() => ({
- channels: new Map([[1, { id: 1, name: "general", type: "text" as const, category: null, position: 0, unreadCount: 0, lastMessageId: null }]]),
+ channels: new Map([
+ [
+ 1,
+ {
+ id: 1,
+ name: "general",
+ type: "text" as const,
+ category: null,
+ position: 0,
+ unreadCount: 0,
+ lastMessageId: null,
+ },
+ ],
+ ]),
activeChannelId: 1,
roles: [],
}));
@@ -423,7 +440,9 @@ describe("notifyIncomingMessage", () => {
// Remove Notification entirely to trigger the inner catch
const originalNotification = globalThis.Notification;
Object.defineProperty(globalThis, "Notification", {
- get() { throw new Error("Notification not available"); },
+ get() {
+ throw new Error("Notification not available");
+ },
configurable: true,
});
diff --git a/Client/tauri-client/tests/unit/overlay-managers.test.ts b/Client/tauri-client/tests/unit/overlay-managers.test.ts
index 8bfe0f06..af984f7c 100644
--- a/Client/tauri-client/tests/unit/overlay-managers.test.ts
+++ b/Client/tauri-client/tests/unit/overlay-managers.test.ts
@@ -122,7 +122,9 @@ function makeMockApi(overrides: Record = {}) {
],
}),
unpinMessage: vi.fn().mockResolvedValue(undefined),
- search: vi.fn().mockResolvedValue({ results: [{ channel_id: 1, message_id: 10, content: "hello" }] }),
+ search: vi
+ .fn()
+ .mockResolvedValue({ results: [{ channel_id: 1, message_id: 10, content: "hello" }] }),
...overrides,
};
}
@@ -155,7 +157,6 @@ describe("createInviteManagerController", () => {
const controller = createInviteManagerController({
api: api as never,
getRoot: () => root,
-
});
await controller.open();
@@ -173,7 +174,6 @@ describe("createInviteManagerController", () => {
const controller = createInviteManagerController({
api: api as never,
getRoot: () => root,
-
});
await controller.open();
@@ -197,7 +197,6 @@ describe("createInviteManagerController", () => {
const controller = createInviteManagerController({
api: api as never,
getRoot: () => root,
-
});
await controller.open();
@@ -219,7 +218,6 @@ describe("createInviteManagerController", () => {
const controller = createInviteManagerController({
api: api as never,
getRoot: () => root,
-
});
await controller.open();
@@ -365,10 +363,7 @@ describe("createPinnedPanelController", () => {
opts.onJumpToMessage(999);
expect(mockScrollToMessage).toHaveBeenCalledWith(999);
- expect(mockShowToast).toHaveBeenCalledWith(
- expect.stringContaining("not in"),
- "info",
- );
+ expect(mockShowToast).toHaveBeenCalledWith(expect.stringContaining("not in"), "info");
// Panel should NOT close when message not found
expect(mockPinnedMessagesDestroy).not.toHaveBeenCalled();
});
@@ -963,7 +958,8 @@ describe("createInviteManagerController (additional)", () => {
it("onRevokeInvite succeeds when invite code is not found in re-fetch", async () => {
const api = makeMockApi({
- getInvites: vi.fn()
+ getInvites: vi
+ .fn()
.mockResolvedValueOnce([makeInviteResponse()]) // initial load
.mockResolvedValueOnce([]), // re-fetch returns empty
});
@@ -1076,7 +1072,11 @@ describe("createSearchOverlayController", () => {
controller.open();
const opts = (createSearchOverlay as Mock).mock.calls[0]![0] as {
- onSearch: (query: string, chId: number | undefined, signal?: AbortSignal) => Promise;
+ onSearch: (
+ query: string,
+ chId: number | undefined,
+ signal?: AbortSignal,
+ ) => Promise;
};
const results = await opts.onSearch("hello", 5);
@@ -1099,7 +1099,11 @@ describe("createSearchOverlayController", () => {
controller.open();
const opts = (createSearchOverlay as Mock).mock.calls[0]![0] as {
- onSearch: (query: string, chId: number | undefined, signal?: AbortSignal) => Promise;
+ onSearch: (
+ query: string,
+ chId: number | undefined,
+ signal?: AbortSignal,
+ ) => Promise;
};
await expect(opts.onSearch("test", 5)).rejects.toThrow("Aborted");
@@ -1121,7 +1125,11 @@ describe("createSearchOverlayController", () => {
controller.open();
const opts = (createSearchOverlay as Mock).mock.calls[0]![0] as {
- onSearch: (query: string, chId: number | undefined, signal?: AbortSignal) => Promise;
+ onSearch: (
+ query: string,
+ chId: number | undefined,
+ signal?: AbortSignal,
+ ) => Promise;
};
await expect(opts.onSearch("test", 5)).rejects.toThrow("network failure");
@@ -1148,7 +1156,10 @@ describe("createSearchOverlayController", () => {
// Mock requestAnimationFrame to execute immediately
const origRaf = globalThis.requestAnimationFrame;
- globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => { cb(0); return 0; };
+ globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => {
+ cb(0);
+ return 0;
+ };
opts.onSelectResult({ channel_id: 3, message_id: 42 });
@@ -1176,7 +1187,10 @@ describe("createSearchOverlayController", () => {
};
const origRaf = globalThis.requestAnimationFrame;
- globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => { cb(0); return 0; };
+ globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => {
+ cb(0);
+ return 0;
+ };
opts.onSelectResult({ channel_id: 3, message_id: 999 });
diff --git a/Client/tauri-client/tests/unit/permissions.test.ts b/Client/tauri-client/tests/unit/permissions.test.ts
index 98aa5ebf..36640a52 100644
--- a/Client/tauri-client/tests/unit/permissions.test.ts
+++ b/Client/tauri-client/tests/unit/permissions.test.ts
@@ -1,35 +1,35 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect } from "vitest";
import {
hasPermission,
hasAnyPermission,
hasAllPermissions,
computeEffective,
isAdministrator,
-} from '../../src/lib/permissions';
-import { Permission } from '../../src/lib/types';
+} from "../../src/lib/permissions";
+import { Permission } from "../../src/lib/types";
// Default role permission values (from SCHEMA.md)
-const OWNER_PERMS = 0x7FFFFFFF;
-const ADMIN_PERMS = 0x3FFFFFFF; // admin has bits 0-29 but NOT ADMINISTRATOR (bit 30)
-const MODERATOR_PERMS = 0x000FFFFF;
+const OWNER_PERMS = 0x7fffffff;
+const ADMIN_PERMS = 0x3fffffff; // admin has bits 0-29 but NOT ADMINISTRATOR (bit 30)
+const MODERATOR_PERMS = 0x000fffff;
const MEMBER_PERMS = 0x00000663;
-describe('hasPermission', () => {
- it('member can SEND_MESSAGES', () => {
+describe("hasPermission", () => {
+ it("member can SEND_MESSAGES", () => {
expect(hasPermission(MEMBER_PERMS, Permission.SEND_MESSAGES)).toBe(true);
});
- it('member cannot MANAGE_MESSAGES', () => {
+ it("member cannot MANAGE_MESSAGES", () => {
expect(hasPermission(MEMBER_PERMS, Permission.MANAGE_MESSAGES)).toBe(false);
});
- it('ADMINISTRATOR bypass — admin with ADMINISTRATOR can do anything', () => {
+ it("ADMINISTRATOR bypass — admin with ADMINISTRATOR can do anything", () => {
const permsWithAdmin = Permission.ADMINISTRATOR;
expect(hasPermission(permsWithAdmin, Permission.MANAGE_MESSAGES)).toBe(true);
expect(hasPermission(permsWithAdmin, Permission.BAN_MEMBERS)).toBe(true);
});
- it('owner has all permissions', () => {
+ it("owner has all permissions", () => {
expect(hasPermission(OWNER_PERMS, Permission.SEND_MESSAGES)).toBe(true);
expect(hasPermission(OWNER_PERMS, Permission.MANAGE_SERVER)).toBe(true);
expect(hasPermission(OWNER_PERMS, Permission.VIEW_AUDIT_LOG)).toBe(true);
@@ -37,28 +37,20 @@ describe('hasPermission', () => {
});
});
-describe('hasAnyPermission', () => {
- it('returns true if any match', () => {
+describe("hasAnyPermission", () => {
+ it("returns true if any match", () => {
expect(
- hasAnyPermission(
- MEMBER_PERMS,
- Permission.SEND_MESSAGES,
- Permission.MANAGE_MESSAGES,
- ),
+ hasAnyPermission(MEMBER_PERMS, Permission.SEND_MESSAGES, Permission.MANAGE_MESSAGES),
).toBe(true);
});
- it('returns false if none match', () => {
- expect(
- hasAnyPermission(
- MEMBER_PERMS,
- Permission.MANAGE_MESSAGES,
- Permission.BAN_MEMBERS,
- ),
- ).toBe(false);
+ it("returns false if none match", () => {
+ expect(hasAnyPermission(MEMBER_PERMS, Permission.MANAGE_MESSAGES, Permission.BAN_MEMBERS)).toBe(
+ false,
+ );
});
- it('ADMINISTRATOR bypass — always returns true', () => {
+ it("ADMINISTRATOR bypass — always returns true", () => {
expect(
hasAnyPermission(
Permission.ADMINISTRATOR,
@@ -68,7 +60,7 @@ describe('hasAnyPermission', () => {
).toBe(true);
});
- it('returns true when only one of many perms matches', () => {
+ it("returns true when only one of many perms matches", () => {
expect(
hasAnyPermission(
MEMBER_PERMS,
@@ -79,35 +71,25 @@ describe('hasAnyPermission', () => {
).toBe(true);
});
- it('returns false with zero permissions', () => {
- expect(
- hasAnyPermission(0, Permission.SEND_MESSAGES, Permission.READ_MESSAGES),
- ).toBe(false);
+ it("returns false with zero permissions", () => {
+ expect(hasAnyPermission(0, Permission.SEND_MESSAGES, Permission.READ_MESSAGES)).toBe(false);
});
});
-describe('hasAllPermissions', () => {
- it('returns true when all match', () => {
+describe("hasAllPermissions", () => {
+ it("returns true when all match", () => {
expect(
- hasAllPermissions(
- MEMBER_PERMS,
- Permission.SEND_MESSAGES,
- Permission.READ_MESSAGES,
- ),
+ hasAllPermissions(MEMBER_PERMS, Permission.SEND_MESSAGES, Permission.READ_MESSAGES),
).toBe(true);
});
- it('returns false when one missing', () => {
+ it("returns false when one missing", () => {
expect(
- hasAllPermissions(
- MEMBER_PERMS,
- Permission.SEND_MESSAGES,
- Permission.MANAGE_MESSAGES,
- ),
+ hasAllPermissions(MEMBER_PERMS, Permission.SEND_MESSAGES, Permission.MANAGE_MESSAGES),
).toBe(false);
});
- it('ADMINISTRATOR bypass — always returns true', () => {
+ it("ADMINISTRATOR bypass — always returns true", () => {
expect(
hasAllPermissions(
Permission.ADMINISTRATOR,
@@ -118,19 +100,17 @@ describe('hasAllPermissions', () => {
).toBe(true);
});
- it('returns false when zero perms and checking multiple', () => {
- expect(
- hasAllPermissions(0, Permission.SEND_MESSAGES, Permission.READ_MESSAGES),
- ).toBe(false);
+ it("returns false when zero perms and checking multiple", () => {
+ expect(hasAllPermissions(0, Permission.SEND_MESSAGES, Permission.READ_MESSAGES)).toBe(false);
});
- it('returns true with no permissions to check (vacuous truth)', () => {
+ it("returns true with no permissions to check (vacuous truth)", () => {
expect(hasAllPermissions(MEMBER_PERMS)).toBe(true);
});
});
-describe('computeEffective', () => {
- it('allow overrides deny (allow-wins, matches server semantics)', () => {
+describe("computeEffective", () => {
+ it("allow overrides deny (allow-wins, matches server semantics)", () => {
const base = MEMBER_PERMS;
const allow = Permission.MANAGE_MESSAGES;
const deny = Permission.MANAGE_MESSAGES;
@@ -138,22 +118,22 @@ describe('computeEffective', () => {
expect(effective & Permission.MANAGE_MESSAGES).toBe(Permission.MANAGE_MESSAGES);
});
- it('ADMINISTRATOR ignores deny and returns all bits', () => {
+ it("ADMINISTRATOR ignores deny and returns all bits", () => {
// Must use a perm set that actually includes bit 30 (ADMINISTRATOR)
const base = OWNER_PERMS; // 0x7FFFFFFF includes ADMINISTRATOR
const deny = Permission.SEND_MESSAGES | Permission.MANAGE_SERVER;
const effective = computeEffective(base, 0, deny);
- expect(effective).toBe(0x7FFFFFFF);
+ expect(effective).toBe(0x7fffffff);
});
- it('non-ADMINISTRATOR admin is affected by deny', () => {
+ it("non-ADMINISTRATOR admin is affected by deny", () => {
// ADMIN_PERMS (0x3FFFFFFF) does NOT have ADMINISTRATOR bit
const deny = Permission.SEND_MESSAGES;
const effective = computeEffective(ADMIN_PERMS, 0, deny);
expect(effective & Permission.SEND_MESSAGES).toBe(0);
});
- it('allow adds bits to base', () => {
+ it("allow adds bits to base", () => {
const base = MEMBER_PERMS;
const allow = Permission.MANAGE_MESSAGES;
const effective = computeEffective(base, allow, 0);
@@ -163,43 +143,43 @@ describe('computeEffective', () => {
});
});
-describe('isAdministrator', () => {
- it('true for owner with ADMINISTRATOR bit', () => {
+describe("isAdministrator", () => {
+ it("true for owner with ADMINISTRATOR bit", () => {
expect(isAdministrator(OWNER_PERMS)).toBe(true);
});
- it('false for admin without ADMINISTRATOR bit', () => {
+ it("false for admin without ADMINISTRATOR bit", () => {
// ADMIN_PERMS (0x3FFFFFFF) has bits 0-29 but NOT bit 30
expect(isAdministrator(ADMIN_PERMS)).toBe(false);
});
- it('false for member', () => {
+ it("false for member", () => {
expect(isAdministrator(MEMBER_PERMS)).toBe(false);
});
- it('false for zero permissions', () => {
+ it("false for zero permissions", () => {
expect(isAdministrator(0)).toBe(false);
});
- it('true for exactly ADMINISTRATOR bit only', () => {
+ it("true for exactly ADMINISTRATOR bit only", () => {
expect(isAdministrator(Permission.ADMINISTRATOR)).toBe(true);
});
});
-describe('edge cases', () => {
- it('hasPermission with zero perms returns false', () => {
+describe("edge cases", () => {
+ it("hasPermission with zero perms returns false", () => {
expect(hasPermission(0, Permission.SEND_MESSAGES)).toBe(false);
});
- it('hasPermission checking ADMINISTRATOR bit directly', () => {
+ it("hasPermission checking ADMINISTRATOR bit directly", () => {
expect(hasPermission(Permission.ADMINISTRATOR, Permission.ADMINISTRATOR)).toBe(true);
});
- it('computeEffective with zero base, allow, deny', () => {
+ it("computeEffective with zero base, allow, deny", () => {
expect(computeEffective(0, 0, 0)).toBe(0);
});
- it('computeEffective deny removes bits from base', () => {
+ it("computeEffective deny removes bits from base", () => {
const base = Permission.SEND_MESSAGES | Permission.READ_MESSAGES;
const deny = Permission.SEND_MESSAGES;
const effective = computeEffective(base, 0, deny);
@@ -207,7 +187,7 @@ describe('edge cases', () => {
expect(effective & Permission.READ_MESSAGES).toBe(Permission.READ_MESSAGES);
});
- it('computeEffective with allow and deny for different bits', () => {
+ it("computeEffective with allow and deny for different bits", () => {
const base = Permission.SEND_MESSAGES;
const allow = Permission.MANAGE_MESSAGES;
const deny = Permission.SEND_MESSAGES;
@@ -216,19 +196,19 @@ describe('edge cases', () => {
expect(effective & Permission.MANAGE_MESSAGES).toBe(Permission.MANAGE_MESSAGES);
});
- it('hasAnyPermission with single matching perm', () => {
+ it("hasAnyPermission with single matching perm", () => {
expect(hasAnyPermission(Permission.SEND_MESSAGES, Permission.SEND_MESSAGES)).toBe(true);
});
- it('hasAllPermissions with single matching perm', () => {
+ it("hasAllPermissions with single matching perm", () => {
expect(hasAllPermissions(Permission.SEND_MESSAGES, Permission.SEND_MESSAGES)).toBe(true);
});
- it('moderator has KICK_MEMBERS', () => {
+ it("moderator has KICK_MEMBERS", () => {
expect(hasPermission(MODERATOR_PERMS, Permission.KICK_MEMBERS)).toBe(true);
});
- it('moderator does not have MANAGE_SERVER', () => {
+ it("moderator does not have MANAGE_SERVER", () => {
expect(hasPermission(MODERATOR_PERMS, Permission.MANAGE_SERVER)).toBe(false);
});
});
diff --git a/Client/tauri-client/tests/unit/pinned-messages.test.ts b/Client/tauri-client/tests/unit/pinned-messages.test.ts
index c4c2c100..8f628a01 100644
--- a/Client/tauri-client/tests/unit/pinned-messages.test.ts
+++ b/Client/tauri-client/tests/unit/pinned-messages.test.ts
@@ -3,9 +3,27 @@ import { createPinnedMessages } from "@components/PinnedMessages";
import type { PinnedMessage, PinnedMessagesOptions } from "@components/PinnedMessages";
const samplePins: PinnedMessage[] = [
- { id: 1, content: "Hello world", author: "Alice", timestamp: "2024-01-01T12:00:00Z", avatarColor: "#5865f2" },
- { id: 2, content: "Important notice", author: "Bob", timestamp: "2024-01-02T14:30:00Z", avatarColor: "#e74c3c" },
- { id: 3, content: "Reminder", author: "Charlie", timestamp: "2024-01-03T09:00:00Z", avatarColor: "#2ecc71" },
+ {
+ id: 1,
+ content: "Hello world",
+ author: "Alice",
+ timestamp: "2024-01-01T12:00:00Z",
+ avatarColor: "#5865f2",
+ },
+ {
+ id: 2,
+ content: "Important notice",
+ author: "Bob",
+ timestamp: "2024-01-02T14:30:00Z",
+ avatarColor: "#e74c3c",
+ },
+ {
+ id: 3,
+ content: "Reminder",
+ author: "Charlie",
+ timestamp: "2024-01-03T09:00:00Z",
+ avatarColor: "#2ecc71",
+ },
];
describe("PinnedMessages", () => {
diff --git a/Client/tauri-client/tests/unit/profiles.test.ts b/Client/tauri-client/tests/unit/profiles.test.ts
index fc1328d8..c3c577dd 100644
--- a/Client/tauri-client/tests/unit/profiles.test.ts
+++ b/Client/tauri-client/tests/unit/profiles.test.ts
@@ -34,10 +34,8 @@ function nextUuid(): string {
function createMockBackend(): PersistenceBackend & {
saved: Array<{ schemaVersion: number; profiles: readonly ServerProfile[] }>;
} {
- let stored: { schemaVersion: number; profiles: readonly ServerProfile[] } | null =
- null;
- const saved: Array<{ schemaVersion: number; profiles: readonly ServerProfile[] }> =
- [];
+ let stored: { schemaVersion: number; profiles: readonly ServerProfile[] } | null = null;
+ const saved: Array<{ schemaVersion: number; profiles: readonly ServerProfile[] }> = [];
return {
saved,
@@ -55,9 +53,7 @@ function createMockBackend(): PersistenceBackend & {
// Mock fetch
// ---------------------------------------------------------------------------
-function createMockFetch(
- handler: (url: string, init?: RequestInit) => Promise,
-): FetchFn {
+function createMockFetch(handler: (url: string, init?: RequestInit) => Promise): FetchFn {
return handler as unknown as FetchFn;
}
@@ -236,9 +232,7 @@ describe("ProfileManager", () => {
describe("health checks", () => {
it("returns online status for a healthy server", async () => {
- const fetchFn = createMockFetch(async () =>
- jsonResponse({ version: "1.2.3" }),
- );
+ const fetchFn = createMockFetch(async () => jsonResponse({ version: "1.2.3" }));
const m = mgr(fetchFn);
const profile = m.addProfile(sampleData);
@@ -296,9 +290,7 @@ describe("ProfileManager", () => {
});
it("returns offline for non-OK response", async () => {
- const fetchFn = createMockFetch(async () =>
- jsonResponse({ error: "bad" }, 500),
- );
+ const fetchFn = createMockFetch(async () => jsonResponse({ error: "bad" }, 500));
const m = mgr(fetchFn);
const profile = m.addProfile(sampleData);
@@ -359,9 +351,7 @@ describe("ProfileManager", () => {
expect(results.get(p2.id)?.status).toBe("online");
expect(pingedHosts).toHaveLength(2);
expect(pingedHosts).toContain("https://localhost:8443/api/v1/health");
- expect(pingedHosts).toContain(
- "https://prod.example.com:443/api/v1/health",
- );
+ expect(pingedHosts).toContain("https://prod.example.com:443/api/v1/health");
});
it("checkAllHealth returns empty map when no profiles", async () => {
@@ -382,10 +372,7 @@ describe("ProfileManager", () => {
const exported = m1.exportProfiles();
const backend2 = createMockBackend();
- const m2 = createProfileManager(
- backend2,
- mockFetch as unknown as FetchFn,
- );
+ const m2 = createProfileManager(backend2, mockFetch as unknown as FetchFn);
const result = m2.importProfiles(exported);
expect(result.imported).toBe(2);
@@ -446,8 +433,26 @@ describe("ProfileManager", () => {
it("rejects import entries with invalid shape", () => {
const m = mgr();
const badEntries = [
- { id: "x", name: "", host: "a", username: "b", color: "#000", autoConnect: false, rememberPassword: false, lastConnected: null },
- { id: "y", name: "Valid", host: "valid.com:443", username: "u", color: "#fff", autoConnect: false, rememberPassword: false, lastConnected: null },
+ {
+ id: "x",
+ name: "",
+ host: "a",
+ username: "b",
+ color: "#000",
+ autoConnect: false,
+ rememberPassword: false,
+ lastConnected: null,
+ },
+ {
+ id: "y",
+ name: "Valid",
+ host: "valid.com:443",
+ username: "u",
+ color: "#fff",
+ autoConnect: false,
+ rememberPassword: false,
+ lastConnected: null,
+ },
];
const result = m.importProfiles(JSON.stringify(badEntries));
expect(result.imported).toBe(1);
@@ -492,10 +497,7 @@ describe("ProfileManager", () => {
const exported = m1.exportProfiles();
const backend2 = createMockBackend();
- const m2 = createProfileManager(
- backend2,
- mockFetch as unknown as FetchFn,
- );
+ const m2 = createProfileManager(backend2, mockFetch as unknown as FetchFn);
m2.importProfiles(exported);
const imported = m2.getAll();
@@ -644,9 +646,7 @@ describe("ProfileManager", () => {
});
it("returns null onlineUsers when field is missing", async () => {
- const fetchFn = createMockFetch(async () =>
- jsonResponse({ version: "1.2.3" }),
- );
+ const fetchFn = createMockFetch(async () => jsonResponse({ version: "1.2.3" }));
const m = mgr(fetchFn);
const profile = m.addProfile(sampleData);
@@ -656,9 +656,7 @@ describe("ProfileManager", () => {
});
it("returns null version when field is not a string", async () => {
- const fetchFn = createMockFetch(async () =>
- jsonResponse({ version: 123 }),
- );
+ const fetchFn = createMockFetch(async () => jsonResponse({ version: 123 }));
const m = mgr(fetchFn);
const profile = m.addProfile(sampleData);
@@ -698,9 +696,7 @@ describe("ProfileManager", () => {
});
it("healthStatuses updates are visible via store", async () => {
- const fetchFn = createMockFetch(async () =>
- jsonResponse({ version: "3.0.0" }),
- );
+ const fetchFn = createMockFetch(async () => jsonResponse({ version: "3.0.0" }));
const m = mgr(fetchFn);
const profile = m.addProfile(sampleData);
diff --git a/Client/tauri-client/tests/unit/ptt.test.ts b/Client/tauri-client/tests/unit/ptt.test.ts
index bb5de3cd..bf29c825 100644
--- a/Client/tauri-client/tests/unit/ptt.test.ts
+++ b/Client/tauri-client/tests/unit/ptt.test.ts
@@ -37,7 +37,9 @@ vi.mock("@tauri-apps/api/event", () => ({
vi.mock("@components/settings/helpers", () => ({
loadPref: (key: string, fallback: unknown) => testPrefs.get(key) ?? fallback,
- savePref: (key: string, value: unknown) => { testPrefs.set(key, value); },
+ savePref: (key: string, value: unknown) => {
+ testPrefs.set(key, value);
+ },
}));
vi.mock("@stores/voice.store", () => ({
@@ -116,15 +118,15 @@ describe("vkName", () => {
});
it("returns 'F12' for 0x7B", () => {
- expect(vkName(0x7B)).toBe("F12");
+ expect(vkName(0x7b)).toBe("F12");
});
it("returns 'Enter' for 0x0D", () => {
- expect(vkName(0x0D)).toBe("Enter");
+ expect(vkName(0x0d)).toBe("Enter");
});
it("returns 'Escape' for 0x1B", () => {
- expect(vkName(0x1B)).toBe("Escape");
+ expect(vkName(0x1b)).toBe("Escape");
});
it("returns 'Backspace' for 0x08", () => {
@@ -136,11 +138,11 @@ describe("vkName", () => {
});
it("returns 'Delete' for 0x2E", () => {
- expect(vkName(0x2E)).toBe("Delete");
+ expect(vkName(0x2e)).toBe("Delete");
});
it("returns 'Insert' for 0x2D", () => {
- expect(vkName(0x2D)).toBe("Insert");
+ expect(vkName(0x2d)).toBe("Insert");
});
it("returns 'Arrow Left' for 0x25", () => {
@@ -196,11 +198,11 @@ describe("vkName", () => {
});
it("returns 'Z' for 0x5A", () => {
- expect(vkName(0x5A)).toBe("Z");
+ expect(vkName(0x5a)).toBe("Z");
});
it("returns 'M' for 0x4D", () => {
- expect(vkName(0x4D)).toBe("M");
+ expect(vkName(0x4d)).toBe("M");
});
});
@@ -221,11 +223,11 @@ describe("vkName", () => {
describe("unknown keys — hex fallback", () => {
it("returns hex string for an unrecognised VK code", () => {
// 0xFF is not in the map and not in any named range
- expect(vkName(0xFF)).toBe("Key 0xFF");
+ expect(vkName(0xff)).toBe("Key 0xFF");
});
it("returns uppercase hex for 0xAB", () => {
- expect(vkName(0xAB)).toBe("Key 0xAB");
+ expect(vkName(0xab)).toBe("Key 0xAB");
});
it("returns 'Key 0x0' for vk code 0", () => {
diff --git a/Client/tauri-client/tests/unit/quick-switcher.test.ts b/Client/tauri-client/tests/unit/quick-switcher.test.ts
index 79bced62..51d19bab 100644
--- a/Client/tauri-client/tests/unit/quick-switcher.test.ts
+++ b/Client/tauri-client/tests/unit/quick-switcher.test.ts
@@ -196,7 +196,9 @@ describe("QuickSwitcher", () => {
it("Ctrl+K closes the switcher when it is open", () => {
switcher.mount(container);
- document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", ctrlKey: true, bubbles: true }));
+ document.dispatchEvent(
+ new KeyboardEvent("keydown", { key: "k", ctrlKey: true, bubbles: true }),
+ );
expect(onClose).toHaveBeenCalledOnce();
});
diff --git a/Client/tauri-client/tests/unit/rate-limiter.test.ts b/Client/tauri-client/tests/unit/rate-limiter.test.ts
index 9eee1bd2..def79686 100644
--- a/Client/tauri-client/tests/unit/rate-limiter.test.ts
+++ b/Client/tauri-client/tests/unit/rate-limiter.test.ts
@@ -34,9 +34,7 @@ describe("RateLimiter", () => {
});
it("throws when windowMs < 1", () => {
- expect(() => new RateLimiter({ maxTokens: 1, windowMs: 0 })).toThrow(
- "windowMs must be >= 1",
- );
+ expect(() => new RateLimiter({ maxTokens: 1, windowMs: 0 })).toThrow("windowMs must be >= 1");
});
// -- tryConsume -----------------------------------------------------------
diff --git a/Client/tauri-client/tests/unit/reaction-controller.test.ts b/Client/tauri-client/tests/unit/reaction-controller.test.ts
index 95b31e07..46995bfd 100644
--- a/Client/tauri-client/tests/unit/reaction-controller.test.ts
+++ b/Client/tauri-client/tests/unit/reaction-controller.test.ts
@@ -4,12 +4,15 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mocks
// ---------------------------------------------------------------------------
-const { mockGetChannelMessages, createMockEmojiPickerElement, mockEmojiPickerDestroy } =
- vi.hoisted(() => ({
- mockGetChannelMessages: vi.fn((): Array<{ id: number; reactions: Array<{ emoji: string; me: boolean }> }> => []),
+const { mockGetChannelMessages, createMockEmojiPickerElement, mockEmojiPickerDestroy } = vi.hoisted(
+ () => ({
+ mockGetChannelMessages: vi.fn(
+ (): Array<{ id: number; reactions: Array<{ emoji: string; me: boolean }> }> => [],
+ ),
createMockEmojiPickerElement: () => document.createElement("div"),
mockEmojiPickerDestroy: vi.fn(),
- }));
+ }),
+);
vi.mock("@lib/dom", () => ({
createElement: vi.fn((tag: string, attrs?: Record) => {
@@ -93,9 +96,7 @@ describe("createReactionController", () => {
describe("direct emoji toggle", () => {
it("sends reaction_add for a new emoji", () => {
- mockGetChannelMessages.mockReturnValue([
- { id: 1, reactions: [] },
- ]);
+ mockGetChannelMessages.mockReturnValue([{ id: 1, reactions: [] }]);
const opts = makeOpts();
const ctrl = createReactionController(opts);
@@ -108,9 +109,7 @@ describe("createReactionController", () => {
});
it("sends reaction_remove when user already reacted", () => {
- mockGetChannelMessages.mockReturnValue([
- { id: 1, reactions: [{ emoji: "👍", me: true }] },
- ]);
+ mockGetChannelMessages.mockReturnValue([{ id: 1, reactions: [{ emoji: "👍", me: true }] }]);
const opts = makeOpts();
const ctrl = createReactionController(opts);
@@ -129,9 +128,7 @@ describe("createReactionController", () => {
ctrl.handleReaction(1, "👍");
expect(opts.ws.send).not.toHaveBeenCalled();
- expect(opts.showError).toHaveBeenCalledWith(
- "Slow down! Please wait before reacting again.",
- );
+ expect(opts.showError).toHaveBeenCalledWith("Slow down! Please wait before reacting again.");
});
});
@@ -149,8 +146,15 @@ describe("createReactionController", () => {
const btn = document.createElement("button");
btn.setAttribute("data-testid", "msg-react-1");
btn.getBoundingClientRect = vi.fn(() => ({
- left: 500, right: 530, top: 100, bottom: 130,
- width: 30, height: 30, x: 500, y: 100, toJSON: () => {},
+ left: 500,
+ right: 530,
+ top: 100,
+ bottom: 130,
+ width: 30,
+ height: 30,
+ x: 500,
+ y: 100,
+ toJSON: () => {},
}));
document.body.appendChild(btn);
@@ -166,8 +170,15 @@ describe("createReactionController", () => {
const btn = document.createElement("button");
btn.setAttribute("data-testid", "msg-react-1");
btn.getBoundingClientRect = vi.fn(() => ({
- left: 500, right: 530, top: 100, bottom: 130,
- width: 30, height: 30, x: 500, y: 100, toJSON: () => {},
+ left: 500,
+ right: 530,
+ top: 100,
+ bottom: 130,
+ width: 30,
+ height: 30,
+ x: 500,
+ y: 100,
+ toJSON: () => {},
}));
document.body.appendChild(btn);
@@ -183,14 +194,19 @@ describe("createReactionController", () => {
});
it("sends reaction when emoji is selected from picker", () => {
- mockGetChannelMessages.mockReturnValue([
- { id: 1, reactions: [] },
- ]);
+ mockGetChannelMessages.mockReturnValue([{ id: 1, reactions: [] }]);
const btn = document.createElement("button");
btn.setAttribute("data-testid", "msg-react-1");
btn.getBoundingClientRect = vi.fn(() => ({
- left: 500, right: 530, top: 100, bottom: 130,
- width: 30, height: 30, x: 500, y: 100, toJSON: () => {},
+ left: 500,
+ right: 530,
+ top: 100,
+ bottom: 130,
+ width: 30,
+ height: 30,
+ x: 500,
+ y: 100,
+ toJSON: () => {},
}));
document.body.appendChild(btn);
@@ -211,14 +227,19 @@ describe("createReactionController", () => {
});
it("calls pickerDestroy when emoji is selected", () => {
- mockGetChannelMessages.mockReturnValue([
- { id: 1, reactions: [] },
- ]);
+ mockGetChannelMessages.mockReturnValue([{ id: 1, reactions: [] }]);
const btn = document.createElement("button");
btn.setAttribute("data-testid", "msg-react-1");
btn.getBoundingClientRect = vi.fn(() => ({
- left: 500, right: 530, top: 100, bottom: 130,
- width: 30, height: 30, x: 500, y: 100, toJSON: () => {},
+ left: 500,
+ right: 530,
+ top: 100,
+ bottom: 130,
+ width: 30,
+ height: 30,
+ x: 500,
+ y: 100,
+ toJSON: () => {},
}));
document.body.appendChild(btn);
@@ -235,8 +256,15 @@ describe("createReactionController", () => {
const btn = document.createElement("button");
btn.setAttribute("data-testid", "msg-react-1");
btn.getBoundingClientRect = vi.fn(() => ({
- left: 500, right: 530, top: 100, bottom: 130,
- width: 30, height: 30, x: 500, y: 100, toJSON: () => {},
+ left: 500,
+ right: 530,
+ top: 100,
+ bottom: 130,
+ width: 30,
+ height: 30,
+ x: 500,
+ y: 100,
+ toJSON: () => {},
}));
document.body.appendChild(btn);
@@ -254,8 +282,15 @@ describe("createReactionController", () => {
const btn = document.createElement("button");
btn.setAttribute("data-testid", "msg-react-1");
btn.getBoundingClientRect = vi.fn(() => ({
- left: 500, right: 530, top: 100, bottom: 130,
- width: 30, height: 30, x: 500, y: 100, toJSON: () => {},
+ left: 500,
+ right: 530,
+ top: 100,
+ bottom: 130,
+ width: 30,
+ height: 30,
+ x: 500,
+ y: 100,
+ toJSON: () => {},
}));
document.body.appendChild(btn);
@@ -275,8 +310,15 @@ describe("createReactionController", () => {
const btn = document.createElement("button");
btn.setAttribute("data-testid", "msg-react-1");
btn.getBoundingClientRect = vi.fn(() => ({
- left: 500, right: 530, top: 100, bottom: 130,
- width: 30, height: 30, x: 500, y: 100, toJSON: () => {},
+ left: 500,
+ right: 530,
+ top: 100,
+ bottom: 130,
+ width: 30,
+ height: 30,
+ x: 500,
+ y: 100,
+ toJSON: () => {},
}));
document.body.appendChild(btn);
diff --git a/Client/tauri-client/tests/unit/reconcile.test.ts b/Client/tauri-client/tests/unit/reconcile.test.ts
index 46b031f7..2075b9ec 100644
--- a/Client/tauri-client/tests/unit/reconcile.test.ts
+++ b/Client/tauri-client/tests/unit/reconcile.test.ts
@@ -1,5 +1,5 @@
-import { describe, it, expect, vi } from 'vitest';
-import { reconcileList } from '../../src/lib/reconcile';
+import { describe, it, expect, vi } from "vitest";
+import { reconcileList } from "../../src/lib/reconcile";
interface Item {
id: string;
@@ -7,7 +7,7 @@ interface Item {
}
function makeContainer(): HTMLDivElement {
- return document.createElement('div');
+ return document.createElement("div");
}
function makeItem(id: string, label: string): Item {
@@ -15,9 +15,9 @@ function makeItem(id: string, label: string): Item {
}
function createEl(item: Item): HTMLDivElement {
- const el = document.createElement('div');
+ const el = document.createElement("div");
el.textContent = item.label;
- el.setAttribute('data-reconcile-key', item.id);
+ el.setAttribute("data-reconcile-key", item.id);
return el;
}
@@ -26,15 +26,13 @@ function updateEl(el: Element, item: Item): void {
}
function getKeys(container: Element): string[] {
- return Array.from(container.children).map(
- (c) => c.getAttribute('data-reconcile-key') ?? '',
- );
+ return Array.from(container.children).map((c) => c.getAttribute("data-reconcile-key") ?? "");
}
-describe('reconcileList', () => {
- it('inserts new items into empty container', () => {
+describe("reconcileList", () => {
+ it("inserts new items into empty container", () => {
const container = makeContainer();
- const items = [makeItem('a', 'A'), makeItem('b', 'B')];
+ const items = [makeItem("a", "A"), makeItem("b", "B")];
reconcileList({
container,
@@ -45,14 +43,14 @@ describe('reconcileList', () => {
});
expect(container.children.length).toBe(2);
- expect(getKeys(container)).toEqual(['a', 'b']);
- expect(container.children[0]!.textContent).toBe('A');
- expect(container.children[1]!.textContent).toBe('B');
+ expect(getKeys(container)).toEqual(["a", "b"]);
+ expect(container.children[0]!.textContent).toBe("A");
+ expect(container.children[1]!.textContent).toBe("B");
});
- it('removes deleted items', () => {
+ it("removes deleted items", () => {
const container = makeContainer();
- const items = [makeItem('a', 'A'), makeItem('b', 'B'), makeItem('c', 'C')];
+ const items = [makeItem("a", "A"), makeItem("b", "B"), makeItem("c", "C")];
reconcileList({
container,
@@ -66,19 +64,19 @@ describe('reconcileList', () => {
// Remove 'b'
reconcileList({
container,
- items: [makeItem('a', 'A'), makeItem('c', 'C')],
+ items: [makeItem("a", "A"), makeItem("c", "C")],
key: (i) => i.id,
create: createEl,
update: updateEl,
});
expect(container.children.length).toBe(2);
- expect(getKeys(container)).toEqual(['a', 'c']);
+ expect(getKeys(container)).toEqual(["a", "c"]);
});
- it('reorders moved items', () => {
+ it("reorders moved items", () => {
const container = makeContainer();
- const items = [makeItem('a', 'A'), makeItem('b', 'B'), makeItem('c', 'C')];
+ const items = [makeItem("a", "A"), makeItem("b", "B"), makeItem("c", "C")];
reconcileList({
container,
@@ -91,18 +89,18 @@ describe('reconcileList', () => {
// Reverse order
reconcileList({
container,
- items: [makeItem('c', 'C'), makeItem('b', 'B'), makeItem('a', 'A')],
+ items: [makeItem("c", "C"), makeItem("b", "B"), makeItem("a", "A")],
key: (i) => i.id,
create: createEl,
update: updateEl,
});
- expect(getKeys(container)).toEqual(['c', 'b', 'a']);
+ expect(getKeys(container)).toEqual(["c", "b", "a"]);
});
- it('updates changed items in-place (preserves DOM reference)', () => {
+ it("updates changed items in-place (preserves DOM reference)", () => {
const container = makeContainer();
- const items = [makeItem('a', 'A'), makeItem('b', 'B')];
+ const items = [makeItem("a", "A"), makeItem("b", "B")];
reconcileList({
container,
@@ -118,7 +116,7 @@ describe('reconcileList', () => {
// Update label for 'a'
reconcileList({
container,
- items: [makeItem('a', 'A-updated'), makeItem('b', 'B')],
+ items: [makeItem("a", "A-updated"), makeItem("b", "B")],
key: (i) => i.id,
create: createEl,
update: updateEl,
@@ -127,10 +125,10 @@ describe('reconcileList', () => {
// SAME DOM elements — not rebuilt
expect(container.children[0]).toBe(origA);
expect(container.children[1]).toBe(origB);
- expect(origA.textContent).toBe('A-updated');
+ expect(origA.textContent).toBe("A-updated");
});
- it('handles empty → items', () => {
+ it("handles empty → items", () => {
const container = makeContainer();
reconcileList({
@@ -144,21 +142,21 @@ describe('reconcileList', () => {
reconcileList({
container,
- items: [makeItem('x', 'X')],
+ items: [makeItem("x", "X")],
key: (i) => i.id,
create: createEl,
update: updateEl,
});
expect(container.children.length).toBe(1);
- expect(getKeys(container)).toEqual(['x']);
+ expect(getKeys(container)).toEqual(["x"]);
});
- it('handles items → empty', () => {
+ it("handles items → empty", () => {
const container = makeContainer();
reconcileList({
container,
- items: [makeItem('a', 'A'), makeItem('b', 'B')],
+ items: [makeItem("a", "A"), makeItem("b", "B")],
key: (i) => i.id,
create: createEl,
update: updateEl,
@@ -175,9 +173,9 @@ describe('reconcileList', () => {
expect(container.children.length).toBe(0);
});
- it('no-op when identical items', () => {
+ it("no-op when identical items", () => {
const container = makeContainer();
- const items = [makeItem('a', 'A'), makeItem('b', 'B')];
+ const items = [makeItem("a", "A"), makeItem("b", "B")];
const createSpy = vi.fn(createEl);
reconcileList({
@@ -195,7 +193,7 @@ describe('reconcileList', () => {
// Same items again
reconcileList({
container,
- items: [makeItem('a', 'A'), makeItem('b', 'B')],
+ items: [makeItem("a", "A"), makeItem("b", "B")],
key: (i) => i.id,
create: createSpy,
update: updateEl,
@@ -208,12 +206,12 @@ describe('reconcileList', () => {
expect(container.children[1]).toBe(origB);
});
- it('handles simultaneous add, remove, and reorder', () => {
+ it("handles simultaneous add, remove, and reorder", () => {
const container = makeContainer();
reconcileList({
container,
- items: [makeItem('a', 'A'), makeItem('b', 'B'), makeItem('c', 'C')],
+ items: [makeItem("a", "A"), makeItem("b", "B"), makeItem("c", "C")],
key: (i) => i.id,
create: createEl,
update: updateEl,
@@ -224,13 +222,13 @@ describe('reconcileList', () => {
// Remove 'a', add 'd', reorder: c, d, b
reconcileList({
container,
- items: [makeItem('c', 'C'), makeItem('d', 'D'), makeItem('b', 'B')],
+ items: [makeItem("c", "C"), makeItem("d", "D"), makeItem("b", "B")],
key: (i) => i.id,
create: createEl,
update: updateEl,
});
- expect(getKeys(container)).toEqual(['c', 'd', 'b']);
+ expect(getKeys(container)).toEqual(["c", "d", "b"]);
expect(container.children.length).toBe(3);
// 'c' element preserved
expect(container.children[0]).toBe(origC);
diff --git a/Client/tauri-client/tests/unit/renderers.test.ts b/Client/tauri-client/tests/unit/renderers.test.ts
index f9f41315..a8893361 100644
--- a/Client/tauri-client/tests/unit/renderers.test.ts
+++ b/Client/tauri-client/tests/unit/renderers.test.ts
@@ -272,11 +272,21 @@ describe("renderers", () => {
const el = renderMessage(msg, false, [msg], makeOpts(), ac.signal);
container.appendChild(el);
- expect(container.querySelector("[data-testid='msg-react-1']")?.getAttribute("aria-label")).toBe("React");
- expect(container.querySelector("[data-testid='msg-reply-1']")?.getAttribute("aria-label")).toBe("Reply");
- expect(container.querySelector("[data-testid='msg-pin-1']")?.getAttribute("aria-label")).toBe("Pin");
- expect(container.querySelector("[data-testid='msg-edit-1']")?.getAttribute("aria-label")).toBe("Edit");
- expect(container.querySelector("[data-testid='msg-delete-1']")?.getAttribute("aria-label")).toBe("Delete");
+ expect(
+ container.querySelector("[data-testid='msg-react-1']")?.getAttribute("aria-label"),
+ ).toBe("React");
+ expect(
+ container.querySelector("[data-testid='msg-reply-1']")?.getAttribute("aria-label"),
+ ).toBe("Reply");
+ expect(container.querySelector("[data-testid='msg-pin-1']")?.getAttribute("aria-label")).toBe(
+ "Pin",
+ );
+ expect(
+ container.querySelector("[data-testid='msg-edit-1']")?.getAttribute("aria-label"),
+ ).toBe("Edit");
+ expect(
+ container.querySelector("[data-testid='msg-delete-1']")?.getAttribute("aria-label"),
+ ).toBe("Delete");
ac.abort();
});
@@ -287,7 +297,9 @@ describe("renderers", () => {
const el = renderMessage(msg, false, [msg], makeOpts(), ac.signal);
container.appendChild(el);
- expect(container.querySelector("[data-testid='msg-pin-1']")?.getAttribute("aria-label")).toBe("Unpin");
+ expect(container.querySelector("[data-testid='msg-pin-1']")?.getAttribute("aria-label")).toBe(
+ "Unpin",
+ );
ac.abort();
});
@@ -324,7 +336,13 @@ describe("renderers", () => {
it("renders attachments for image types", () => {
const msg = makeMessage({
attachments: [
- { id: "1", filename: "photo.png", size: 1024, mime: "image/png", url: "/uploads/photo.png" },
+ {
+ id: "1",
+ filename: "photo.png",
+ size: 1024,
+ mime: "image/png",
+ url: "/uploads/photo.png",
+ },
],
});
const ac = new AbortController();
@@ -339,7 +357,13 @@ describe("renderers", () => {
it("renders attachments for file types", () => {
const msg = makeMessage({
attachments: [
- { id: "1", filename: "doc.pdf", size: 2048, mime: "application/pdf", url: "/uploads/doc.pdf" },
+ {
+ id: "1",
+ filename: "doc.pdf",
+ size: 2048,
+ mime: "application/pdf",
+ url: "/uploads/doc.pdf",
+ },
],
});
const ac = new AbortController();
@@ -401,11 +425,7 @@ describe("renderers", () => {
});
it("returns a valid HH:MM string for every supported timestamp format", () => {
- const formats = [
- "2026-03-19T08:30:00Z",
- "2026-03-19 08:30:00",
- "2026-03-19T08:30:00+00:00",
- ];
+ const formats = ["2026-03-19T08:30:00Z", "2026-03-19 08:30:00", "2026-03-19T08:30:00+00:00"];
for (const ts of formats) {
expect(formatTime(ts)).toMatch(/^\d{2}:\d{2}$/);
}
@@ -695,9 +715,11 @@ describe("renderers", () => {
// loadPref reads from localStorage with the "owncord:settings:" prefix
localStorage.setItem("owncord:settings:developerMode", "true");
// Dispatch pref-change to invalidate the cached developerModeEnabled value
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "developerMode" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "developerMode" },
+ }),
+ );
const msg = makeMessage();
const ac = new AbortController();
@@ -710,17 +732,21 @@ describe("renderers", () => {
// Clean up: restore developer mode to false
localStorage.setItem("owncord:settings:developerMode", "false");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "developerMode" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "developerMode" },
+ }),
+ );
ac.abort();
});
it("Copy ID button calls clipboard.writeText on click", () => {
localStorage.setItem("owncord:settings:developerMode", "true");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "developerMode" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "developerMode" },
+ }),
+ );
const writeTextMock = vi.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText: writeTextMock } });
@@ -738,17 +764,21 @@ describe("renderers", () => {
// Clean up
localStorage.setItem("owncord:settings:developerMode", "false");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "developerMode" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "developerMode" },
+ }),
+ );
ac.abort();
});
it("Copy ID button handles clipboard failure gracefully", () => {
localStorage.setItem("owncord:settings:developerMode", "true");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "developerMode" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "developerMode" },
+ }),
+ );
Object.assign(navigator, {
clipboard: { writeText: vi.fn().mockRejectedValue(new Error("clipboard unavailable")) },
@@ -765,17 +795,21 @@ describe("renderers", () => {
// Clean up
localStorage.setItem("owncord:settings:developerMode", "false");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "developerMode" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "developerMode" },
+ }),
+ );
ac.abort();
});
it("does not render Copy ID button when developerMode is disabled", () => {
localStorage.setItem("owncord:settings:developerMode", "false");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "developerMode" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "developerMode" },
+ }),
+ );
const msg = makeMessage();
const ac = new AbortController();
@@ -988,7 +1022,9 @@ describe("renderers", () => {
it("returns role for known member", () => {
membersStore.setState((prev) => ({
...prev,
- members: new Map([[42, { id: 42, username: "admin", avatar: null, role: "admin", status: "online" }]]),
+ members: new Map([
+ [42, { id: 42, username: "admin", avatar: null, role: "admin", status: "online" }],
+ ]),
}));
expect(getUserRole(42)).toBe("admin");
});
@@ -998,9 +1034,11 @@ describe("renderers", () => {
afterEach(() => {
// Restore roleColors to default (true)
localStorage.setItem("owncord:settings:roleColors", "true");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "roleColors" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "roleColors" },
+ }),
+ );
});
it("returns owner color for 'owner' role", () => {
@@ -1025,9 +1063,11 @@ describe("renderers", () => {
it("returns member color for all roles when roleColors is disabled", () => {
localStorage.setItem("owncord:settings:roleColors", "false");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "roleColors" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "roleColors" },
+ }),
+ );
expect(roleColorVar("owner")).toBe("var(--role-member)");
expect(roleColorVar("admin")).toBe("var(--role-member)");
@@ -1037,15 +1077,19 @@ describe("renderers", () => {
it("re-enables role colors when pref changes back to true", () => {
localStorage.setItem("owncord:settings:roleColors", "false");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "roleColors" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "roleColors" },
+ }),
+ );
expect(roleColorVar("owner")).toBe("var(--role-member)");
localStorage.setItem("owncord:settings:roleColors", "true");
- window.dispatchEvent(new CustomEvent("owncord:pref-change", {
- detail: { key: "roleColors" },
- }));
+ window.dispatchEvent(
+ new CustomEvent("owncord:pref-change", {
+ detail: { key: "roleColors" },
+ }),
+ );
expect(roleColorVar("owner")).toBe("var(--role-owner)");
});
});
@@ -1201,7 +1245,6 @@ describe("renderers", () => {
expect(container.textContent).toContain("before");
expect(container.textContent).toContain("after");
});
-
});
// ---------------------------------------------------------------------------
diff --git a/Client/tauri-client/tests/unit/rnnoise-worklet.test.ts b/Client/tauri-client/tests/unit/rnnoise-worklet.test.ts
index 652802a9..b62d3998 100644
--- a/Client/tauri-client/tests/unit/rnnoise-worklet.test.ts
+++ b/Client/tauri-client/tests/unit/rnnoise-worklet.test.ts
@@ -4,20 +4,22 @@ const REGISTERED_NAME = "rnnoise-processor";
describe("rnnoise-worklet", () => {
let registerProcessorMock: ReturnType;
- let processorCtor: (new () => {
- _processFrame(): void;
- _outAvailable: number;
- _outReadPos: number;
- _outWritePos: number;
- _outSampleOffset: number;
- _inputPtr: number;
- _outputPtr: number;
- _state: number;
- _inputRing: Float32Array;
- _outBuffer: Float32Array;
- _heapF32: Float32Array | null;
- _instance: { exports: { rnnoise_process_frame: ReturnType } } | null;
- }) | null;
+ let processorCtor:
+ | (new () => {
+ _processFrame(): void;
+ _outAvailable: number;
+ _outReadPos: number;
+ _outWritePos: number;
+ _outSampleOffset: number;
+ _inputPtr: number;
+ _outputPtr: number;
+ _state: number;
+ _inputRing: Float32Array;
+ _outBuffer: Float32Array;
+ _heapF32: Float32Array | null;
+ _instance: { exports: { rnnoise_process_frame: ReturnType } } | null;
+ })
+ | null;
beforeEach(() => {
vi.resetModules();
@@ -78,4 +80,4 @@ describe("rnnoise-worklet", () => {
expect(processor._outReadPos).toBe(4);
expect(processor._outSampleOffset).toBe(0);
});
-});
\ No newline at end of file
+});
diff --git a/Client/tauri-client/tests/unit/search-overlay.test.ts b/Client/tauri-client/tests/unit/search-overlay.test.ts
index e6839290..c0c80b69 100644
--- a/Client/tauri-client/tests/unit/search-overlay.test.ts
+++ b/Client/tauri-client/tests/unit/search-overlay.test.ts
@@ -220,10 +220,7 @@ describe("createSearchOverlay", () => {
describe("keyboard navigation", () => {
it("ArrowDown moves active index", async () => {
- const results = [
- makeResult({ message_id: 1 }),
- makeResult({ message_id: 2 }),
- ];
+ const results = [makeResult({ message_id: 1 }), makeResult({ message_id: 2 })];
const onSearch = vi.fn().mockResolvedValue(results);
const opts = makeOptions({ onSearch });
const overlay = createSearchOverlay(opts);
@@ -235,12 +232,20 @@ describe("createSearchOverlay", () => {
await vi.advanceTimersByTimeAsync(300);
// First item should be active
- expect(container.querySelector("[data-testid='search-result-0']")!.classList.contains("search-result-item--active")).toBe(true);
+ expect(
+ container
+ .querySelector("[data-testid='search-result-0']")!
+ .classList.contains("search-result-item--active"),
+ ).toBe(true);
// Arrow down
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
- expect(container.querySelector("[data-testid='search-result-1']")!.classList.contains("search-result-item--active")).toBe(true);
+ expect(
+ container
+ .querySelector("[data-testid='search-result-1']")!
+ .classList.contains("search-result-item--active"),
+ ).toBe(true);
overlay.destroy?.();
});
@@ -354,20 +359,25 @@ describe("createSearchOverlay", () => {
await vi.advanceTimersByTimeAsync(300);
// First item is active
- expect(container.querySelector("[data-testid='search-result-0']")!.classList.contains("search-result-item--active")).toBe(true);
+ expect(
+ container
+ .querySelector("[data-testid='search-result-0']")!
+ .classList.contains("search-result-item--active"),
+ ).toBe(true);
// Arrow up should wrap to last
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }));
- expect(container.querySelector("[data-testid='search-result-2']")!.classList.contains("search-result-item--active")).toBe(true);
+ expect(
+ container
+ .querySelector("[data-testid='search-result-2']")!
+ .classList.contains("search-result-item--active"),
+ ).toBe(true);
overlay.destroy?.();
});
it("ArrowDown wraps from last to first", async () => {
- const results = [
- makeResult({ message_id: 1 }),
- makeResult({ message_id: 2 }),
- ];
+ const results = [makeResult({ message_id: 1 }), makeResult({ message_id: 2 })];
const onSearch = vi.fn().mockResolvedValue(results);
const opts = makeOptions({ onSearch });
const overlay = createSearchOverlay(opts);
@@ -380,11 +390,19 @@ describe("createSearchOverlay", () => {
// Move to last
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
- expect(container.querySelector("[data-testid='search-result-1']")!.classList.contains("search-result-item--active")).toBe(true);
+ expect(
+ container
+ .querySelector("[data-testid='search-result-1']")!
+ .classList.contains("search-result-item--active"),
+ ).toBe(true);
// One more should wrap to first
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
- expect(container.querySelector("[data-testid='search-result-0']")!.classList.contains("search-result-item--active")).toBe(true);
+ expect(
+ container
+ .querySelector("[data-testid='search-result-0']")!
+ .classList.contains("search-result-item--active"),
+ ).toBe(true);
overlay.destroy?.();
});
@@ -393,10 +411,12 @@ describe("createSearchOverlay", () => {
it("aborts previous search when a new search starts", async () => {
let abortedSignal: AbortSignal | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const onSearch = vi.fn().mockImplementation((_q: string, _ch: number | undefined, signal: AbortSignal) => {
- abortedSignal = signal;
- return new Promise(() => {}); // Never resolves — stalled search
- });
+ const onSearch = vi
+ .fn()
+ .mockImplementation((_q: string, _ch: number | undefined, signal: AbortSignal) => {
+ abortedSignal = signal;
+ return new Promise(() => {}); // Never resolves — stalled search
+ });
const opts = makeOptions({ onSearch });
const overlay = createSearchOverlay(opts);
overlay.mount(container);
@@ -411,7 +431,9 @@ describe("createSearchOverlay", () => {
const firstSignal = abortedSignal;
// Second search — should abort the first
+ // Advance past the MIN_SEARCH_INTERVAL_MS (500ms) rate limit before triggering debounce
onSearch.mockImplementation(() => Promise.resolve([]));
+ await vi.advanceTimersByTimeAsync(200); // now 500ms since first search fired
input.value = "second";
input.dispatchEvent(new Event("input"));
await vi.advanceTimersByTimeAsync(300);
@@ -425,8 +447,11 @@ describe("createSearchOverlay", () => {
it("shows 'Searching...' status during search", async () => {
let resolveSearch: ((results: SearchResultItem[]) => void) | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const onSearch = vi.fn().mockImplementation(() =>
- new Promise((resolve) => { resolveSearch = resolve; }),
+ const onSearch = vi.fn().mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveSearch = resolve;
+ }),
);
const opts = makeOptions({ onSearch });
const overlay = createSearchOverlay(opts);
diff --git a/Client/tauri-client/tests/unit/server-panel.test.ts b/Client/tauri-client/tests/unit/server-panel.test.ts
index dd337a91..f619dc32 100644
--- a/Client/tauri-client/tests/unit/server-panel.test.ts
+++ b/Client/tauri-client/tests/unit/server-panel.test.ts
@@ -193,10 +193,7 @@ describe("ServerPanel", () => {
describe("server click", () => {
it("calls onServerClick with host when a server item is clicked", () => {
const onServerClick = vi.fn();
- const panel = createServerPanel(
- makeOpts({ onServerClick }),
- [SIMPLE_PROFILES[0]!],
- );
+ const panel = createServerPanel(makeOpts({ onServerClick }), [SIMPLE_PROFILES[0]!]);
container.appendChild(panel.element);
const item = container.querySelector(".server-item") as HTMLElement;
@@ -208,10 +205,7 @@ describe("ServerPanel", () => {
it("calls onServerClick with host AND username for full profiles", () => {
const onServerClick = vi.fn();
const fp = fullProfile();
- const panel = createServerPanel(
- makeOpts({ onServerClick }),
- [fp],
- );
+ const panel = createServerPanel(makeOpts({ onServerClick }), [fp]);
container.appendChild(panel.element);
const item = container.querySelector(".server-item") as HTMLElement;
@@ -240,21 +234,15 @@ describe("ServerPanel", () => {
token: "tok",
});
- const panel = createServerPanel(
- makeOpts({ onCredentialLoaded }),
- [SIMPLE_PROFILES[0]!],
- );
+ const panel = createServerPanel(makeOpts({ onCredentialLoaded }), [SIMPLE_PROFILES[0]!]);
container.appendChild(panel.element);
const item = container.querySelector(".server-item") as HTMLElement;
item.click();
+ // Password is no longer returned from credential store over IPC (security hardening)
await vi.waitFor(() => {
- expect(onCredentialLoaded).toHaveBeenCalledWith(
- "localhost:8443",
- "saveduser",
- "savedpass",
- );
+ expect(onCredentialLoaded).toHaveBeenCalledWith("localhost:8443", "saveduser", undefined);
});
});
@@ -262,10 +250,7 @@ describe("ServerPanel", () => {
const onCredentialLoaded = vi.fn();
vi.mocked(loadCredential).mockResolvedValueOnce(null);
- const panel = createServerPanel(
- makeOpts({ onCredentialLoaded }),
- [SIMPLE_PROFILES[0]!],
- );
+ const panel = createServerPanel(makeOpts({ onCredentialLoaded }), [SIMPLE_PROFILES[0]!]);
container.appendChild(panel.element);
const item = container.querySelector(".server-item") as HTMLElement;
@@ -338,10 +323,7 @@ describe("ServerPanel", () => {
it("calls onToggleAutoLogin with profile id and toggled state", () => {
const onToggleAutoLogin = vi.fn();
const fp = fullProfile({ autoConnect: false });
- const panel = createServerPanel(
- makeOpts({ onToggleAutoLogin }),
- [fp],
- );
+ const panel = createServerPanel(makeOpts({ onToggleAutoLogin }), [fp]);
container.appendChild(panel.element);
const autoLoginBtn = container.querySelector(".auto-login") as HTMLElement;
@@ -354,10 +336,7 @@ describe("ServerPanel", () => {
const onServerClick = vi.fn();
const onToggleAutoLogin = vi.fn();
const fp = fullProfile();
- const panel = createServerPanel(
- makeOpts({ onServerClick, onToggleAutoLogin }),
- [fp],
- );
+ const panel = createServerPanel(makeOpts({ onServerClick, onToggleAutoLogin }), [fp]);
container.appendChild(panel.element);
const autoLoginBtn = container.querySelector(".auto-login") as HTMLElement;
@@ -416,10 +395,7 @@ describe("ServerPanel", () => {
it("calls onDeleteProfile with profile id on click", () => {
const onDeleteProfile = vi.fn();
const fp = fullProfile();
- const panel = createServerPanel(
- makeOpts({ onDeleteProfile }),
- [fp],
- );
+ const panel = createServerPanel(makeOpts({ onDeleteProfile }), [fp]);
container.appendChild(panel.element);
const deleteBtn = container.querySelector(".srv-btn.danger") as HTMLElement;
@@ -432,10 +408,7 @@ describe("ServerPanel", () => {
const onServerClick = vi.fn();
const onDeleteProfile = vi.fn();
const fp = fullProfile();
- const panel = createServerPanel(
- makeOpts({ onServerClick, onDeleteProfile }),
- [fp],
- );
+ const panel = createServerPanel(makeOpts({ onServerClick, onDeleteProfile }), [fp]);
container.appendChild(panel.element);
const deleteBtn = container.querySelector(".srv-btn.danger") as HTMLElement;
diff --git a/Client/tauri-client/tests/unit/settings-overlay.test.ts b/Client/tauri-client/tests/unit/settings-overlay.test.ts
index 0d7706cf..d6d3f4ad 100644
--- a/Client/tauri-client/tests/unit/settings-overlay.test.ts
+++ b/Client/tauri-client/tests/unit/settings-overlay.test.ts
@@ -108,7 +108,9 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(defaultOptions);
overlay.mount(container);
- const activeTab = container.querySelector(".settings-sidebar > button.settings-nav-item.active");
+ const activeTab = container.querySelector(
+ ".settings-sidebar > button.settings-nav-item.active",
+ );
expect(activeTab?.textContent).toBe("Account");
overlay.destroy?.();
@@ -344,8 +346,9 @@ describe("SettingsOverlay", () => {
(inputs[1] as HTMLInputElement).value = "short";
(inputs[2] as HTMLInputElement).value = "short";
- const changePwBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Change Password") as HTMLElement;
+ const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Change Password",
+ ) as HTMLElement;
changePwBtn.click();
expect(defaultOptions.onChangePassword).not.toHaveBeenCalled();
@@ -362,8 +365,9 @@ describe("SettingsOverlay", () => {
(inputs[1] as HTMLInputElement).value = "newpassword123";
(inputs[2] as HTMLInputElement).value = "differentpassword";
- const changePwBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Change Password") as HTMLElement;
+ const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Change Password",
+ ) as HTMLElement;
changePwBtn.click();
expect(defaultOptions.onChangePassword).not.toHaveBeenCalled();
@@ -381,8 +385,9 @@ describe("SettingsOverlay", () => {
(inputs[1] as HTMLInputElement).value = "newpassword123";
(inputs[2] as HTMLInputElement).value = "newpassword123";
- const changePwBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Change Password") as HTMLElement;
+ const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Change Password",
+ ) as HTMLElement;
changePwBtn.click();
await vi.waitFor(() => {
@@ -409,14 +414,17 @@ describe("SettingsOverlay", () => {
(inputs[1] as HTMLInputElement).value = "newpassword123";
(inputs[2] as HTMLInputElement).value = "newpassword123";
- const changePwBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Change Password") as HTMLElement;
+ const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Change Password",
+ ) as HTMLElement;
changePwBtn.click();
await vi.waitFor(() => {
// Find the error element near the password fields
const errorEls = container.querySelectorAll("div[style*='color:var(--red)']");
- const pwError = Array.from(errorEls).find((el) => el.textContent === "Incorrect old password");
+ const pwError = Array.from(errorEls).find(
+ (el) => el.textContent === "Incorrect old password",
+ );
expect(pwError).not.toBeUndefined();
});
@@ -434,8 +442,9 @@ describe("SettingsOverlay", () => {
const editInput = container.querySelector("input.form-input[type='text']") as HTMLInputElement;
editInput.value = "taken-name";
- const saveBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Save") as HTMLElement;
+ const saveBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Save",
+ ) as HTMLElement;
saveBtn.click();
await vi.waitFor(() => {
@@ -452,8 +461,9 @@ describe("SettingsOverlay", () => {
overlay.mount(container);
// Click "Edit User Profile" button instead of "Edit" button
- const editProfileBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Edit User Profile") as HTMLElement;
+ const editProfileBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Edit User Profile",
+ ) as HTMLElement;
editProfileBtn.click();
const editInput = container.querySelector("input.form-input[type='text']") as HTMLInputElement;
@@ -474,8 +484,9 @@ describe("SettingsOverlay", () => {
editBtn.click();
// Click cancel
- const cancelBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Cancel") as HTMLElement;
+ const cancelBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Cancel",
+ ) as HTMLElement;
cancelBtn.click();
// Edit form should be hidden
@@ -492,10 +503,14 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(defaultOptions);
overlay.mount(container);
- const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement;
+ const triggerBtn = container.querySelector(
+ "[data-testid='delete-account-trigger']",
+ ) as HTMLElement;
expect(triggerBtn).not.toBeNull();
- const confirmArea = container.querySelector("[data-testid='delete-account-confirm-area']") as HTMLElement;
+ const confirmArea = container.querySelector(
+ "[data-testid='delete-account-confirm-area']",
+ ) as HTMLElement;
expect(confirmArea.style.display).toBe("none");
triggerBtn.click();
@@ -510,10 +525,14 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(defaultOptions);
overlay.mount(container);
- const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement;
+ const triggerBtn = container.querySelector(
+ "[data-testid='delete-account-trigger']",
+ ) as HTMLElement;
triggerBtn.click();
- const confirmArea = container.querySelector("[data-testid='delete-account-confirm-area']") as HTMLElement;
+ const confirmArea = container.querySelector(
+ "[data-testid='delete-account-confirm-area']",
+ ) as HTMLElement;
expect(confirmArea.style.display).toBe("block");
const cancelBtn = confirmArea.querySelector("button:not(.account-delete-btn)") as HTMLElement;
@@ -529,10 +548,14 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(defaultOptions);
overlay.mount(container);
- const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement;
+ const triggerBtn = container.querySelector(
+ "[data-testid='delete-account-trigger']",
+ ) as HTMLElement;
triggerBtn.click();
- const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLElement;
+ const confirmBtn = container.querySelector(
+ "[data-testid='delete-account-confirm']",
+ ) as HTMLElement;
confirmBtn.click();
const errorEl = container.querySelector("[data-testid='delete-account-error']") as HTMLElement;
@@ -546,13 +569,19 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(defaultOptions);
overlay.mount(container);
- const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement;
+ const triggerBtn = container.querySelector(
+ "[data-testid='delete-account-trigger']",
+ ) as HTMLElement;
triggerBtn.click();
- const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement;
+ const passwordInput = container.querySelector(
+ "[data-testid='delete-account-password']",
+ ) as HTMLInputElement;
passwordInput.value = "mypassword123";
- const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLButtonElement;
+ const confirmBtn = container.querySelector(
+ "[data-testid='delete-account-confirm']",
+ ) as HTMLButtonElement;
confirmBtn.click();
expect(defaultOptions.onDeleteAccount).toHaveBeenCalledWith("mypassword123");
@@ -564,13 +593,19 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(defaultOptions);
overlay.mount(container);
- const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement;
+ const triggerBtn = container.querySelector(
+ "[data-testid='delete-account-trigger']",
+ ) as HTMLElement;
triggerBtn.click();
- const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement;
+ const passwordInput = container.querySelector(
+ "[data-testid='delete-account-password']",
+ ) as HTMLInputElement;
passwordInput.value = "mypassword123";
- const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLButtonElement;
+ const confirmBtn = container.querySelector(
+ "[data-testid='delete-account-confirm']",
+ ) as HTMLButtonElement;
confirmBtn.click();
expect(confirmBtn.disabled).toBe(true);
@@ -588,13 +623,19 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(failOptions);
overlay.mount(container);
- const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement;
+ const triggerBtn = container.querySelector(
+ "[data-testid='delete-account-trigger']",
+ ) as HTMLElement;
triggerBtn.click();
- const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement;
+ const passwordInput = container.querySelector(
+ "[data-testid='delete-account-password']",
+ ) as HTMLInputElement;
passwordInput.value = "wrongpassword";
- const confirmBtn = container.querySelector("[data-testid='delete-account-confirm']") as HTMLButtonElement;
+ const confirmBtn = container.querySelector(
+ "[data-testid='delete-account-confirm']",
+ ) as HTMLButtonElement;
confirmBtn.click();
// Wait for the rejected promise to settle
@@ -613,14 +654,20 @@ describe("SettingsOverlay", () => {
const overlay = createSettingsOverlay(defaultOptions);
overlay.mount(container);
- const triggerBtn = container.querySelector("[data-testid='delete-account-trigger']") as HTMLElement;
+ const triggerBtn = container.querySelector(
+ "[data-testid='delete-account-trigger']",
+ ) as HTMLElement;
triggerBtn.click();
- const passwordInput = container.querySelector("[data-testid='delete-account-password']") as HTMLInputElement;
+ const passwordInput = container.querySelector(
+ "[data-testid='delete-account-password']",
+ ) as HTMLInputElement;
passwordInput.value = "typed-something";
// Cancel and reopen
- const confirmArea = container.querySelector("[data-testid='delete-account-confirm-area']") as HTMLElement;
+ const confirmArea = container.querySelector(
+ "[data-testid='delete-account-confirm-area']",
+ ) as HTMLElement;
const cancelBtn = confirmArea.querySelector("button:not(.account-delete-btn)") as HTMLElement;
cancelBtn.click();
triggerBtn.click();
@@ -663,8 +710,9 @@ describe("SettingsOverlay", () => {
editInput.value = "A";
// Click Save
- const saveBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Save") as HTMLElement;
+ const saveBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Save",
+ ) as HTMLElement;
saveBtn.click();
// Should NOT call onUpdateProfile
@@ -683,8 +731,9 @@ describe("SettingsOverlay", () => {
const editInput = container.querySelector("input.form-input[type='text']") as HTMLInputElement;
editInput.value = "AB";
- const saveBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Save") as HTMLElement;
+ const saveBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Save",
+ ) as HTMLElement;
saveBtn.click();
expect(defaultOptions.onUpdateProfile).toHaveBeenCalledWith("AB");
@@ -764,7 +813,9 @@ describe("SettingsOverlay", () => {
const tabNames = Array.from(tabs).map((t) => t.textContent);
expect(tabNames).not.toContain("Account");
// Should start on Appearance instead
- const activeTab = container.querySelector(".settings-sidebar > button.settings-nav-item.active");
+ const activeTab = container.querySelector(
+ ".settings-sidebar > button.settings-nav-item.active",
+ );
expect(activeTab?.textContent).toBe("Appearance");
overlay.destroy?.();
diff --git a/Client/tauri-client/tests/unit/sidebar-dm-helpers.test.ts b/Client/tauri-client/tests/unit/sidebar-dm-helpers.test.ts
index c6e6affd..fce393a8 100644
--- a/Client/tauri-client/tests/unit/sidebar-dm-helpers.test.ts
+++ b/Client/tauri-client/tests/unit/sidebar-dm-helpers.test.ts
@@ -216,7 +216,9 @@ describe("SidebarDmHelpers", () => {
it("sets activeDmUserId in UI store", () => {
const deps = makeDeps();
- const dm = makeDmChannel({ recipient: { id: 10, username: "Alice", avatar: "", status: "online" } });
+ const dm = makeDmChannel({
+ recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
+ });
selectDmConversation(dm, deps);
expect(uiStore.getState().activeDmUserId).toBe(10);
@@ -278,7 +280,9 @@ describe("SidebarDmHelpers", () => {
// Add a member with a known status
membersStore.setState((prev) => ({
...prev,
- members: new Map([[20, { id: 20, username: "Bob", avatar: null, role: "member", status: "idle" as const }]]),
+ members: new Map([
+ [20, { id: 20, username: "Bob", avatar: null, role: "member", status: "idle" as const }],
+ ]),
}));
const mockApi = {
@@ -365,13 +369,15 @@ describe("SidebarDmHelpers", () => {
});
it("maps DM channels to DmConversation objects", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- recipient: { id: 10, username: "Alice", avatar: "alice.png", status: "online" },
- lastMessage: "Hello!",
- lastMessageAt: "2025-01-01T00:00:00Z",
- unreadCount: 3,
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ recipient: { id: 10, username: "Alice", avatar: "alice.png", status: "online" },
+ lastMessage: "Hello!",
+ lastMessageAt: "2025-01-01T00:00:00Z",
+ unreadCount: 3,
+ }),
+ );
const result = buildDmConversations(null);
expect(result).toHaveLength(1);
@@ -388,74 +394,95 @@ describe("SidebarDmHelpers", () => {
});
it("marks conversation as active when userId matches activeDmUserId", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
+ }),
+ );
const result = buildDmConversations(10);
expect(result[0]!.active).toBe(true);
});
it("does not mark conversation as active when userId does not match", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
+ }),
+ );
const result = buildDmConversations(999);
expect(result[0]!.active).toBe(false);
});
it("uses 'No messages yet' when lastMessage is empty", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- lastMessage: "",
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ lastMessage: "",
+ }),
+ );
const result = buildDmConversations(null);
expect(result[0]!.lastMessage).toBe("No messages yet");
});
it("sets unread to false when unreadCount is 0", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- unreadCount: 0,
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ unreadCount: 0,
+ }),
+ );
const result = buildDmConversations(null);
expect(result[0]!.unread).toBe(false);
});
it("uses avatar null when avatar is empty string", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
+ }),
+ );
const result = buildDmConversations(null);
expect(result[0]!.avatar).toBeNull();
});
it("defaults status to 'offline' when status is undefined", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- recipient: { id: 10, username: "Alice", avatar: "", status: undefined as unknown as string },
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ recipient: {
+ id: 10,
+ username: "Alice",
+ avatar: "",
+ status: undefined as unknown as string,
+ },
+ }),
+ );
const result = buildDmConversations(null);
expect(result[0]!.status).toBe("offline");
});
it("handles multiple DM channels", () => {
- addDmChannel(makeDmChannel({
- channelId: 100,
- recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
- }));
- addDmChannel(makeDmChannel({
- channelId: 101,
- recipient: { id: 11, username: "Bob", avatar: "", status: "idle" },
- }));
+ addDmChannel(
+ makeDmChannel({
+ channelId: 100,
+ recipient: { id: 10, username: "Alice", avatar: "", status: "online" },
+ }),
+ );
+ addDmChannel(
+ makeDmChannel({
+ channelId: 101,
+ recipient: { id: 11, username: "Bob", avatar: "", status: "idle" },
+ }),
+ );
const result = buildDmConversations(11);
expect(result).toHaveLength(2);
diff --git a/Client/tauri-client/tests/unit/store.test.ts b/Client/tauri-client/tests/unit/store.test.ts
index d97e9114..7b2227ad 100644
--- a/Client/tauri-client/tests/unit/store.test.ts
+++ b/Client/tauri-client/tests/unit/store.test.ts
@@ -1,30 +1,30 @@
-import { describe, it, expect, vi } from 'vitest';
-import { createStore, shallowEqual } from '../../src/lib/store';
+import { describe, it, expect, vi } from "vitest";
+import { createStore, shallowEqual } from "../../src/lib/store";
interface TestState {
count: number;
name: string;
}
-const initialState: TestState = { count: 0, name: 'test' };
+const initialState: TestState = { count: 0, name: "test" };
function freshStore() {
return createStore({ ...initialState });
}
-describe('createStore', () => {
- it('getState returns initial state', () => {
+describe("createStore", () => {
+ it("getState returns initial state", () => {
const store = freshStore();
- expect(store.getState()).toEqual({ count: 0, name: 'test' });
+ expect(store.getState()).toEqual({ count: 0, name: "test" });
});
- it('setState updates state via updater function', () => {
+ it("setState updates state via updater function", () => {
const store = freshStore();
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
- expect(store.getState()).toEqual({ count: 1, name: 'test' });
+ expect(store.getState()).toEqual({ count: 1, name: "test" });
});
- it('setState calls all subscribers with new state', () => {
+ it("setState calls all subscribers with new state", () => {
const store = freshStore();
const listener1 = vi.fn();
const listener2 = vi.fn();
@@ -35,12 +35,12 @@ describe('createStore', () => {
store.flush();
expect(listener1).toHaveBeenCalledTimes(1);
- expect(listener1).toHaveBeenCalledWith({ count: 5, name: 'test' });
+ expect(listener1).toHaveBeenCalledWith({ count: 5, name: "test" });
expect(listener2).toHaveBeenCalledTimes(1);
- expect(listener2).toHaveBeenCalledWith({ count: 5, name: 'test' });
+ expect(listener2).toHaveBeenCalledWith({ count: 5, name: "test" });
});
- it('subscribe returns unsubscribe function that works', () => {
+ it("subscribe returns unsubscribe function that works", () => {
const store = freshStore();
const listener = vi.fn();
const unsubscribe = store.subscribe(listener);
@@ -56,7 +56,7 @@ describe('createStore', () => {
expect(listener).toHaveBeenCalledTimes(1);
});
- it('multiple subscribers all get called', () => {
+ it("multiple subscribers all get called", () => {
const store = freshStore();
const calls: number[] = [];
store.subscribe(() => calls.push(1));
@@ -69,7 +69,7 @@ describe('createStore', () => {
expect(calls).toEqual([1, 2, 3]);
});
- it('unsubscribed listener does not get called', () => {
+ it("unsubscribed listener does not get called", () => {
const store = freshStore();
const kept = vi.fn();
const removed = vi.fn();
@@ -85,7 +85,7 @@ describe('createStore', () => {
expect(removed).not.toHaveBeenCalled();
});
- it('select derives value from state', () => {
+ it("select derives value from state", () => {
const store = freshStore();
store.setState((prev) => ({ ...prev, count: 42 }));
@@ -93,38 +93,38 @@ describe('createStore', () => {
const name = store.select((s) => s.name);
expect(count).toBe(42);
- expect(name).toBe('test');
+ expect(name).toBe("test");
});
- it('setState does NOT mutate previous state reference', () => {
+ it("setState does NOT mutate previous state reference", () => {
const store = freshStore();
const before = store.getState();
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
const after = store.getState();
- expect(before).toEqual({ count: 0, name: 'test' });
- expect(after).toEqual({ count: 1, name: 'test' });
+ expect(before).toEqual({ count: 0, name: "test" });
+ expect(after).toEqual({ count: 1, name: "test" });
expect(before).not.toBe(after);
});
- it('subscriber receives new state not old state', () => {
+ it("subscriber receives new state not old state", () => {
const store = freshStore();
const received: TestState[] = [];
store.subscribe((s) => received.push(s));
store.setState((prev) => ({ ...prev, count: 7 }));
store.flush();
- store.setState((prev) => ({ ...prev, name: 'updated' }));
+ store.setState((prev) => ({ ...prev, name: "updated" }));
store.flush();
expect(received).toEqual([
- { count: 7, name: 'test' },
- { count: 7, name: 'updated' },
+ { count: 7, name: "test" },
+ { count: 7, name: "updated" },
]);
});
- it('no subscribers means setState still works without crash', () => {
+ it("no subscribers means setState still works without crash", () => {
const store = freshStore();
expect(() => {
store.setState((prev) => ({ ...prev, count: 100 }));
@@ -137,8 +137,8 @@ describe('createStore', () => {
// subscribeSelector
// ---------------------------------------------------------------------------
-describe('subscribeSelector', () => {
- it('fires when selected slice changes', () => {
+describe("subscribeSelector", () => {
+ it("fires when selected slice changes", () => {
const store = freshStore();
const listener = vi.fn();
store.subscribeSelector((s) => s.count, listener);
@@ -150,19 +150,19 @@ describe('subscribeSelector', () => {
expect(listener).toHaveBeenCalledWith(5);
});
- it('does NOT fire when selected slice is unchanged', () => {
+ it("does NOT fire when selected slice is unchanged", () => {
const store = freshStore();
const listener = vi.fn();
store.subscribeSelector((s) => s.count, listener);
// Change name but not count
- store.setState((prev) => ({ ...prev, name: 'updated' }));
+ store.setState((prev) => ({ ...prev, name: "updated" }));
store.flush();
expect(listener).not.toHaveBeenCalled();
});
- it('fires only for the changed slice among multiple selectors', () => {
+ it("fires only for the changed slice among multiple selectors", () => {
const store = freshStore();
const countListener = vi.fn();
const nameListener = vi.fn();
@@ -176,7 +176,7 @@ describe('subscribeSelector', () => {
expect(nameListener).not.toHaveBeenCalled();
});
- it('returns unsubscribe function', () => {
+ it("returns unsubscribe function", () => {
const store = freshStore();
const listener = vi.fn();
const unsub = store.subscribeSelector((s) => s.count, listener);
@@ -192,7 +192,7 @@ describe('subscribeSelector', () => {
expect(listener).toHaveBeenCalledTimes(1); // no new call
});
- it('works with custom equality comparator', () => {
+ it("works with custom equality comparator", () => {
const store = freshStore();
const listener = vi.fn();
// Custom comparator: only fire when count changes by more than 5
@@ -212,7 +212,7 @@ describe('subscribeSelector', () => {
expect(listener).toHaveBeenCalledWith(10);
});
- it('works with microtask batching', () => {
+ it("works with microtask batching", () => {
const store = freshStore();
const listener = vi.fn();
store.subscribeSelector((s) => s.count, listener);
@@ -227,30 +227,33 @@ describe('subscribeSelector', () => {
expect(listener).toHaveBeenCalledWith(3);
});
- it('multiple selectors on the same store work independently', () => {
+ it("multiple selectors on the same store work independently", () => {
const store = freshStore();
const results: string[] = [];
- store.subscribeSelector((s) => s.count, (c) => results.push(`count:${c}`));
- store.subscribeSelector((s) => s.name, (n) => results.push(`name:${n}`));
+ store.subscribeSelector(
+ (s) => s.count,
+ (c) => results.push(`count:${c}`),
+ );
+ store.subscribeSelector(
+ (s) => s.name,
+ (n) => results.push(`name:${n}`),
+ );
- store.setState((prev) => ({ ...prev, count: 1, name: 'updated' }));
+ store.setState((prev) => ({ ...prev, count: 1, name: "updated" }));
store.flush();
- expect(results).toEqual(['count:1', 'name:updated']);
+ expect(results).toEqual(["count:1", "name:updated"]);
});
- it('shallow-equal default prevents firing for structurally identical selectors', () => {
+ it("shallow-equal default prevents firing for structurally identical selectors", () => {
const store = freshStore();
const listener = vi.fn();
// Selector creates a new object ref each time, but shallowEqual
// detects that the content is unchanged and skips the notification.
- store.subscribeSelector(
- (s) => ({ count: s.count }),
- listener,
- );
+ store.subscribeSelector((s) => ({ count: s.count }), listener);
// Changing just name does NOT fire because { count: 0 } shallow-equals { count: 0 }
- store.setState((prev) => ({ ...prev, name: 'changed' }));
+ store.setState((prev) => ({ ...prev, name: "changed" }));
store.flush();
expect(listener).toHaveBeenCalledTimes(0);
@@ -260,7 +263,7 @@ describe('subscribeSelector', () => {
expect(listener).toHaveBeenCalledTimes(1);
});
- it('allows strict reference equality via custom comparator', () => {
+ it("allows strict reference equality via custom comparator", () => {
const store = freshStore();
const listener = vi.fn();
// Opt in to strict === comparison to get the old behavior
@@ -271,7 +274,7 @@ describe('subscribeSelector', () => {
);
// New object ref with same content DOES fire with strict ===
- store.setState((prev) => ({ ...prev, name: 'changed' }));
+ store.setState((prev) => ({ ...prev, name: "changed" }));
store.flush();
expect(listener).toHaveBeenCalledTimes(1);
});
@@ -281,118 +284,127 @@ describe('subscribeSelector', () => {
// shallowEqual
// ---------------------------------------------------------------------------
-describe('shallowEqual', () => {
- it('returns true for same reference', () => {
+describe("shallowEqual", () => {
+ it("returns true for same reference", () => {
const obj = { a: 1 };
expect(shallowEqual(obj, obj)).toBe(true);
});
- it('returns true for two identical plain objects', () => {
+ it("returns true for two identical plain objects", () => {
expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true);
});
- it('returns false for objects with different values', () => {
+ it("returns false for objects with different values", () => {
expect(shallowEqual({ a: 1 }, { a: 2 })).toBe(false);
});
- it('returns false for objects with different keys count', () => {
+ it("returns false for objects with different keys count", () => {
expect(shallowEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
});
- it('returns false when one side is null', () => {
+ it("returns false when one side is null", () => {
expect(shallowEqual(null, { a: 1 })).toBe(false);
expect(shallowEqual({ a: 1 }, null)).toBe(false);
});
- it('returns true for null === null', () => {
+ it("returns true for null === null", () => {
expect(shallowEqual(null, null)).toBe(true);
});
- it('returns false for non-object types', () => {
+ it("returns false for non-object types", () => {
expect(shallowEqual(1, 2)).toBe(false);
- expect(shallowEqual('a', 'b')).toBe(false);
+ expect(shallowEqual("a", "b")).toBe(false);
});
- it('returns true for same primitive values', () => {
+ it("returns true for same primitive values", () => {
expect(shallowEqual(42, 42)).toBe(true);
- expect(shallowEqual('hello', 'hello')).toBe(true);
+ expect(shallowEqual("hello", "hello")).toBe(true);
});
- it('returns false when comparing object with primitive', () => {
+ it("returns false when comparing object with primitive", () => {
expect(shallowEqual({ a: 1 }, 42 as unknown)).toBe(false);
expect(shallowEqual(42 as unknown, { a: 1 })).toBe(false);
});
// Map comparison
- it('returns true for identical Maps', () => {
- const a = new Map([['x', 1], ['y', 2]]);
- const b = new Map([['x', 1], ['y', 2]]);
+ it("returns true for identical Maps", () => {
+ const a = new Map([
+ ["x", 1],
+ ["y", 2],
+ ]);
+ const b = new Map([
+ ["x", 1],
+ ["y", 2],
+ ]);
expect(shallowEqual(a, b)).toBe(true);
});
- it('returns false for Maps with different size', () => {
- const a = new Map([['x', 1]]);
- const b = new Map([['x', 1], ['y', 2]]);
+ it("returns false for Maps with different size", () => {
+ const a = new Map([["x", 1]]);
+ const b = new Map([
+ ["x", 1],
+ ["y", 2],
+ ]);
expect(shallowEqual(a, b)).toBe(false);
});
- it('returns false for Maps with different keys', () => {
- const a = new Map([['x', 1]]);
- const b = new Map([['y', 1]]);
+ it("returns false for Maps with different keys", () => {
+ const a = new Map([["x", 1]]);
+ const b = new Map([["y", 1]]);
expect(shallowEqual(a, b)).toBe(false);
});
- it('returns false for Maps with different values', () => {
- const a = new Map([['x', 1]]);
- const b = new Map([['x', 2]]);
+ it("returns false for Maps with different values", () => {
+ const a = new Map([["x", 1]]);
+ const b = new Map([["x", 2]]);
expect(shallowEqual(a, b)).toBe(false);
});
// Set comparison
- it('returns true for identical Sets', () => {
+ it("returns true for identical Sets", () => {
const a = new Set([1, 2, 3]);
const b = new Set([1, 2, 3]);
expect(shallowEqual(a, b)).toBe(true);
});
- it('returns false for Sets with different size', () => {
+ it("returns false for Sets with different size", () => {
const a = new Set([1, 2]);
const b = new Set([1, 2, 3]);
expect(shallowEqual(a, b)).toBe(false);
});
- it('returns false for Sets with different values', () => {
+ it("returns false for Sets with different values", () => {
const a = new Set([1, 2]);
const b = new Set([1, 3]);
expect(shallowEqual(a, b)).toBe(false);
});
// Array comparison
- it('returns true for identical arrays', () => {
+ it("returns true for identical arrays", () => {
expect(shallowEqual([1, 2, 3], [1, 2, 3])).toBe(true);
});
- it('returns false for arrays with different length', () => {
+ it("returns false for arrays with different length", () => {
expect(shallowEqual([1, 2], [1, 2, 3])).toBe(false);
});
- it('returns false for arrays with different elements', () => {
+ it("returns false for arrays with different elements", () => {
expect(shallowEqual([1, 2], [1, 3])).toBe(false);
});
- it('returns true for empty arrays', () => {
+ it("returns true for empty arrays", () => {
expect(shallowEqual([], [])).toBe(true);
});
- it('returns true for empty Maps', () => {
+ it("returns true for empty Maps", () => {
expect(shallowEqual(new Map(), new Map())).toBe(true);
});
- it('returns true for empty Sets', () => {
+ it("returns true for empty Sets", () => {
expect(shallowEqual(new Set(), new Set())).toBe(true);
});
- it('returns true for empty objects', () => {
+ it("returns true for empty objects", () => {
expect(shallowEqual({}, {})).toBe(true);
});
});
@@ -401,8 +413,8 @@ describe('shallowEqual', () => {
// flush edge cases
// ---------------------------------------------------------------------------
-describe('flush edge cases', () => {
- it('flush when no notification is scheduled does nothing', () => {
+describe("flush edge cases", () => {
+ it("flush when no notification is scheduled does nothing", () => {
const store = freshStore();
const listener = vi.fn();
store.subscribe(listener);
@@ -411,7 +423,7 @@ describe('flush edge cases', () => {
expect(listener).not.toHaveBeenCalled();
});
- it('double flush only fires once', () => {
+ it("double flush only fires once", () => {
const store = freshStore();
const listener = vi.fn();
store.subscribe(listener);
@@ -422,11 +434,11 @@ describe('flush edge cases', () => {
expect(listener).toHaveBeenCalledTimes(1);
});
- it('subscribeSelector with Map values uses shallowEqual', () => {
+ it("subscribeSelector with Map values uses shallowEqual", () => {
interface MapState {
items: Map;
}
- const store = createStore({ items: new Map([['a', 1]]) });
+ const store = createStore({ items: new Map([["a", 1]]) });
const listener = vi.fn();
store.subscribeSelector((s) => s.items, listener);
@@ -436,12 +448,12 @@ describe('flush edge cases', () => {
expect(listener).not.toHaveBeenCalled();
// New map with same content -- shallowEqual returns true, so no fire
- store.setState((prev) => ({ items: new Map([['a', 1]]) }));
+ store.setState((prev) => ({ items: new Map([["a", 1]]) }));
store.flush();
expect(listener).not.toHaveBeenCalled();
// Different content
- store.setState(() => ({ items: new Map([['a', 2]]) }));
+ store.setState(() => ({ items: new Map([["a", 2]]) }));
store.flush();
expect(listener).toHaveBeenCalledTimes(1);
});
diff --git a/Client/tauri-client/tests/unit/stored-appearance.test.ts b/Client/tauri-client/tests/unit/stored-appearance.test.ts
index 94cd5315..a2417a33 100644
--- a/Client/tauri-client/tests/unit/stored-appearance.test.ts
+++ b/Client/tauri-client/tests/unit/stored-appearance.test.ts
@@ -1,10 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-const {
- mockGetActiveThemeName,
- mockRestoreTheme,
- mockApplyThemeByName,
-} = vi.hoisted(() => ({
+const { mockGetActiveThemeName, mockRestoreTheme, mockApplyThemeByName } = vi.hoisted(() => ({
mockGetActiveThemeName: vi.fn(() => "neon-glow"),
mockRestoreTheme: vi.fn(),
mockApplyThemeByName: vi.fn(),
@@ -121,4 +117,4 @@ describe("applyStoredAppearance", () => {
expect(mockRestoreTheme).toHaveBeenCalledTimes(1);
expect(mockApplyThemeByName).not.toHaveBeenCalled();
});
-});
\ No newline at end of file
+});
diff --git a/Client/tauri-client/tests/unit/stream-preview.test.ts b/Client/tauri-client/tests/unit/stream-preview.test.ts
index 7d8178f2..78382bdb 100644
--- a/Client/tauri-client/tests/unit/stream-preview.test.ts
+++ b/Client/tauri-client/tests/unit/stream-preview.test.ts
@@ -1,9 +1,11 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
// Mock livekitSession before importing streamPreview
-const mockGetRemoteVideoStream = vi.fn<(uid: number, type: "camera" | "screenshare") => MediaStream | null>();
+const mockGetRemoteVideoStream =
+ vi.fn<(uid: number, type: "camera" | "screenshare") => MediaStream | null>();
vi.mock("@lib/livekitSession", () => ({
- getRemoteVideoStream: (uid: number, type: "camera" | "screenshare") => mockGetRemoteVideoStream(uid, type),
+ getRemoteVideoStream: (uid: number, type: "camera" | "screenshare") =>
+ mockGetRemoteVideoStream(uid, type),
setUserVolume: vi.fn(),
getUserVolume: vi.fn(() => 1),
}));
@@ -27,7 +29,7 @@ beforeAll(() => {
/** Get the preview sibling div after a row (preview is inserted as next sibling). */
function getPreview(row: HTMLElement): HTMLElement | null {
const next = row.nextElementSibling;
- return (next !== null && next.classList.contains("vu-preview")) ? next as HTMLElement : null;
+ return next !== null && next.classList.contains("vu-preview") ? (next as HTMLElement) : null;
}
function createRow(userId: number): HTMLElement {
diff --git a/Client/tauri-client/tests/unit/tenor.test.ts b/Client/tauri-client/tests/unit/tenor.test.ts
index 23c8b085..0891b4a3 100644
--- a/Client/tauri-client/tests/unit/tenor.test.ts
+++ b/Client/tauri-client/tests/unit/tenor.test.ts
@@ -28,7 +28,11 @@ function tenorResult(
title?: string;
} = {},
) {
- const { tinygif = `https://media.tenor.com/${id}_tiny.gif`, gif = `https://media.tenor.com/${id}.gif`, title = `Title ${id}` } = overrides;
+ const {
+ tinygif = `https://media.tenor.com/${id}_tiny.gif`,
+ gif = `https://media.tenor.com/${id}.gif`,
+ title = `Title ${id}`,
+ } = overrides;
const media_formats: Record = {};
if (tinygif !== null) media_formats["tinygif"] = { url: tinygif };
@@ -74,9 +78,7 @@ describe("searchGifs", () => {
it("calls the Tenor search endpoint", async () => {
mockFetch.mockResolvedValue(okResponse([]));
await searchGifs("cats");
- expect(capturedUrl()).toMatch(
- /^https:\/\/tenor\.googleapis\.com\/v2\/search/,
- );
+ expect(capturedUrl()).toMatch(/^https:\/\/tenor\.googleapis\.com\/v2\/search/);
});
it("includes the query param q", async () => {
@@ -125,9 +127,7 @@ describe("searchGifs", () => {
});
it("maps id, title, url (tinygif), and fullUrl (gif) correctly", async () => {
- mockFetch.mockResolvedValue(
- okResponse([tenorResult("abc123")]),
- );
+ mockFetch.mockResolvedValue(okResponse([tenorResult("abc123")]));
const gifs = await searchGifs("cats");
expect(gifs).toHaveLength(1);
expect(gifs[0]).toEqual({
@@ -148,10 +148,7 @@ describe("searchGifs", () => {
it("filters out results with no tinygif format", async () => {
mockFetch.mockResolvedValue(
- okResponse([
- tenorResult("keep"),
- tenorResult("drop", { tinygif: null }),
- ]),
+ okResponse([tenorResult("keep"), tenorResult("drop", { tinygif: null })]),
);
const gifs = await searchGifs("cats");
expect(gifs).toHaveLength(1);
@@ -160,10 +157,7 @@ describe("searchGifs", () => {
it("filters out results with no gif format", async () => {
mockFetch.mockResolvedValue(
- okResponse([
- tenorResult("keep"),
- tenorResult("drop", { gif: null }),
- ]),
+ okResponse([tenorResult("keep"), tenorResult("drop", { gif: null })]),
);
const gifs = await searchGifs("cats");
expect(gifs).toHaveLength(1);
@@ -172,10 +166,7 @@ describe("searchGifs", () => {
it("filters out results missing both formats", async () => {
mockFetch.mockResolvedValue(
- okResponse([
- tenorResult("drop", { tinygif: null, gif: null }),
- tenorResult("keep"),
- ]),
+ okResponse([tenorResult("drop", { tinygif: null, gif: null }), tenorResult("keep")]),
);
const gifs = await searchGifs("cats");
expect(gifs).toHaveLength(1);
@@ -184,10 +175,7 @@ describe("searchGifs", () => {
it("returns an empty array when all results lack required formats", async () => {
mockFetch.mockResolvedValue(
- okResponse([
- tenorResult("x", { tinygif: null }),
- tenorResult("y", { gif: null }),
- ]),
+ okResponse([tenorResult("x", { tinygif: null }), tenorResult("y", { gif: null })]),
);
const gifs = await searchGifs("cats");
expect(gifs).toEqual([]);
@@ -226,9 +214,7 @@ describe("getTrendingGifs", () => {
it("calls the Tenor featured endpoint", async () => {
mockFetch.mockResolvedValue(okResponse([]));
await getTrendingGifs();
- expect(capturedUrl()).toMatch(
- /^https:\/\/tenor\.googleapis\.com\/v2\/featured/,
- );
+ expect(capturedUrl()).toMatch(/^https:\/\/tenor\.googleapis\.com\/v2\/featured/);
});
it("does not include a q param", async () => {
@@ -282,10 +268,7 @@ describe("getTrendingGifs", () => {
it("filters out results with missing tinygif", async () => {
mockFetch.mockResolvedValue(
- okResponse([
- tenorResult("keep"),
- tenorResult("drop", { tinygif: null }),
- ]),
+ okResponse([tenorResult("keep"), tenorResult("drop", { tinygif: null })]),
);
const gifs = await getTrendingGifs();
expect(gifs.map((g) => g.id)).toEqual(["keep"]);
@@ -293,10 +276,7 @@ describe("getTrendingGifs", () => {
it("filters out results with missing gif", async () => {
mockFetch.mockResolvedValue(
- okResponse([
- tenorResult("keep"),
- tenorResult("drop", { gif: null }),
- ]),
+ okResponse([tenorResult("keep"), tenorResult("drop", { gif: null })]),
);
const gifs = await getTrendingGifs();
expect(gifs.map((g) => g.id)).toEqual(["keep"]);
diff --git a/Client/tauri-client/tests/unit/test-harness.test.ts b/Client/tauri-client/tests/unit/test-harness.test.ts
index e1c7bac9..e5b72dd3 100644
--- a/Client/tauri-client/tests/unit/test-harness.test.ts
+++ b/Client/tauri-client/tests/unit/test-harness.test.ts
@@ -40,8 +40,7 @@ describe("createTestHarness", () => {
it("queryAll() returns all matching elements within the container", () => {
harness = createTestHarness();
- harness.container.innerHTML =
- 'ab';
+ harness.container.innerHTML = 'ab';
const els = harness.queryAll(".item");
expect(els.length).toBe(2);
@@ -53,7 +52,9 @@ describe("createTestHarness", () => {
const btn = document.createElement("button");
btn.className = "click-me";
- btn.addEventListener("click", () => { clicked = true; });
+ btn.addEventListener("click", () => {
+ clicked = true;
+ });
harness.container.appendChild(btn);
harness.click(".click-me");
diff --git a/Client/tauri-client/tests/unit/themes.test.ts b/Client/tauri-client/tests/unit/themes.test.ts
index 7edc8e48..5ade2a7d 100644
--- a/Client/tauri-client/tests/unit/themes.test.ts
+++ b/Client/tauri-client/tests/unit/themes.test.ts
@@ -142,7 +142,7 @@ describe("CSS injection prevention", () => {
});
it("should reject property name not starting with --", () => {
- applyCustomWithColors({ "background": "#ff0000" });
+ applyCustomWithColors({ background: "#ff0000" });
// "background" does not start with "--", so it must not be set
expect(document.body.style.getPropertyValue("background")).toBe("");
});
diff --git a/Client/tauri-client/tests/unit/toast-coverage.test.ts b/Client/tauri-client/tests/unit/toast-coverage.test.ts
index 303640aa..3381d3c9 100644
--- a/Client/tauri-client/tests/unit/toast-coverage.test.ts
+++ b/Client/tauri-client/tests/unit/toast-coverage.test.ts
@@ -78,11 +78,7 @@ describe("toast global helper", () => {
initToast(container);
showToast("Server connected", "success");
- expect(container.show).toHaveBeenCalledWith(
- "Server connected",
- "success",
- undefined,
- );
+ expect(container.show).toHaveBeenCalledWith("Server connected", "success", undefined);
});
it("defaults the type to 'info' when not specified", () => {
diff --git a/Client/tauri-client/tests/unit/toast.test.ts b/Client/tauri-client/tests/unit/toast.test.ts
index 8ecb27e8..b09a30b7 100644
--- a/Client/tauri-client/tests/unit/toast.test.ts
+++ b/Client/tauri-client/tests/unit/toast.test.ts
@@ -1,8 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import {
- createToastContainer,
- type ToastContainer,
-} from "../../src/components/Toast";
+import { createToastContainer, type ToastContainer } from "../../src/components/Toast";
describe("ToastContainer", () => {
let container: HTMLDivElement;
diff --git a/Client/tauri-client/tests/unit/totp-settings.test.ts b/Client/tauri-client/tests/unit/totp-settings.test.ts
index 7747b440..5319cbdf 100644
--- a/Client/tauri-client/tests/unit/totp-settings.test.ts
+++ b/Client/tauri-client/tests/unit/totp-settings.test.ts
@@ -113,7 +113,9 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
expect(pwInput).not.toBeNull();
// The enable button should be hidden
expect(enableBtn.style.display).toBe("none");
@@ -133,8 +135,9 @@ describe("TOTP Settings", () => {
enableBtn.click();
// Leave password empty and click Submit
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
const errorEl = container.querySelector("[data-testid='totp-error']") as HTMLElement;
@@ -153,11 +156,14 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
@@ -176,11 +182,14 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
@@ -201,11 +210,14 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
@@ -215,9 +227,7 @@ describe("TOTP Settings", () => {
// Look for the backup codes text
const codeElements = container.querySelectorAll("code");
- const backupCodeEl = Array.from(codeElements).find(
- (el) => el.textContent?.includes("code1"),
- );
+ const backupCodeEl = Array.from(codeElements).find((el) => el.textContent?.includes("code1"));
expect(backupCodeEl).not.toBeUndefined();
expect(backupCodeEl!.textContent).toContain("code1");
expect(backupCodeEl!.textContent).toContain("code2");
@@ -235,15 +245,20 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
- const codeInput = container.querySelector("[data-testid='totp-code-input']") as HTMLInputElement;
+ const codeInput = container.querySelector(
+ "[data-testid='totp-code-input']",
+ ) as HTMLInputElement;
expect(codeInput).not.toBeNull();
expect(codeInput.placeholder).toBe("6-digit code");
});
@@ -266,11 +281,14 @@ describe("TOTP Settings", () => {
enableBtn.click();
// Step 2: Enter password and submit
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
// Wait for QR URI to appear
@@ -279,7 +297,9 @@ describe("TOTP Settings", () => {
});
// Step 3: Enter code and confirm
- const codeInput = container.querySelector("[data-testid='totp-code-input']") as HTMLInputElement;
+ const codeInput = container.querySelector(
+ "[data-testid='totp-code-input']",
+ ) as HTMLInputElement;
codeInput.value = "123456";
const confirmBtn = container.querySelector("[data-testid='totp-confirm-btn']") as HTMLElement;
@@ -303,11 +323,14 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "wrongpassword";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
@@ -334,18 +357,23 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
expect(container.querySelector("[data-testid='totp-qr-uri']")).not.toBeNull();
});
- const codeInput = container.querySelector("[data-testid='totp-code-input']") as HTMLInputElement;
+ const codeInput = container.querySelector(
+ "[data-testid='totp-code-input']",
+ ) as HTMLInputElement;
codeInput.value = "000000";
const confirmBtn = container.querySelector("[data-testid='totp-confirm-btn']") as HTMLElement;
@@ -359,7 +387,9 @@ describe("TOTP Settings", () => {
});
// Confirm button should be re-enabled
- const confirmBtnAfter = container.querySelector("[data-testid='totp-confirm-btn']") as HTMLButtonElement;
+ const confirmBtnAfter = container.querySelector(
+ "[data-testid='totp-confirm-btn']",
+ ) as HTMLButtonElement;
expect(confirmBtnAfter.disabled).toBe(false);
expect(confirmBtnAfter.textContent).toBe("Verify & Activate");
@@ -381,18 +411,23 @@ describe("TOTP Settings", () => {
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
expect(container.querySelector("[data-testid='totp-qr-uri']")).not.toBeNull();
});
- const codeInput = container.querySelector("[data-testid='totp-code-input']") as HTMLInputElement;
+ const codeInput = container.querySelector(
+ "[data-testid='totp-code-input']",
+ ) as HTMLInputElement;
codeInput.value = "123456";
const confirmBtn = container.querySelector("[data-testid='totp-confirm-btn']") as HTMLElement;
@@ -401,7 +436,9 @@ describe("TOTP Settings", () => {
// After successful confirmation, onEnrolled() is called which re-renders.
// Since updateUser sets mockTotpEnabled = true, the re-render should show the disable view.
await vi.waitFor(() => {
- const statusBadge = container.querySelector("[data-testid='totp-status-badge']") as HTMLElement;
+ const statusBadge = container.querySelector(
+ "[data-testid='totp-status-badge']",
+ ) as HTMLElement;
expect(statusBadge.textContent).toBe("Enabled");
});
@@ -448,7 +485,9 @@ describe("TOTP Settings", () => {
expect(disableBtn.style.display).toBe("none");
// Password input should appear
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
expect(pwInput).not.toBeNull();
overlay.destroy?.();
@@ -463,11 +502,14 @@ describe("TOTP Settings", () => {
const disableBtn = container.querySelector("[data-testid='totp-disable-btn']") as HTMLElement;
disableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const confirmBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Confirm Disable") as HTMLElement;
+ const confirmBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Confirm Disable",
+ ) as HTMLElement;
confirmBtn.click();
await vi.waitFor(() => {
@@ -488,11 +530,14 @@ describe("TOTP Settings", () => {
const disableBtn = container.querySelector("[data-testid='totp-disable-btn']") as HTMLElement;
disableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "wrongpassword";
- const confirmBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Confirm Disable") as HTMLElement;
+ const confirmBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Confirm Disable",
+ ) as HTMLElement;
confirmBtn.click();
await vi.waitFor(() => {
@@ -518,18 +563,19 @@ describe("TOTP Settings", () => {
const disableBtn = container.querySelector("[data-testid='totp-disable-btn']") as HTMLElement;
disableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const confirmBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Confirm Disable") as HTMLElement;
+ const confirmBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Confirm Disable",
+ ) as HTMLElement;
confirmBtn.click();
await vi.waitFor(() => {
const errorEl = container.querySelector("[data-testid='totp-error']") as HTMLElement;
- expect(errorEl.textContent).toBe(
- "2FA is required by this server and cannot be disabled",
- );
+ expect(errorEl.textContent).toBe("2FA is required by this server and cannot be disabled");
});
overlay.destroy?.();
@@ -545,14 +591,17 @@ describe("TOTP Settings", () => {
disableBtn.click();
// Confirm area should be visible
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
expect(pwInput).not.toBeNull();
// Find the Cancel button that is a sibling of the "Confirm Disable" button
// inside the TOTP section
const totpSection = container.querySelector("[data-testid='totp-section']") as HTMLElement;
- const cancelBtn = Array.from(totpSection.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Cancel") as HTMLElement;
+ const cancelBtn = Array.from(totpSection.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Cancel",
+ ) as HTMLElement;
cancelBtn.click();
// Disable button should reappear
@@ -577,17 +626,22 @@ describe("TOTP Settings", () => {
const disableBtn = container.querySelector("[data-testid='totp-disable-btn']") as HTMLElement;
disableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const confirmBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Confirm Disable") as HTMLElement;
+ const confirmBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Confirm Disable",
+ ) as HTMLElement;
confirmBtn.click();
// After successful disable, onDisabled() is called which re-renders.
// Since updateUser sets mockTotpEnabled = false, re-render should show enrollment view.
await vi.waitFor(() => {
- const statusBadge = container.querySelector("[data-testid='totp-status-badge']") as HTMLElement;
+ const statusBadge = container.querySelector(
+ "[data-testid='totp-status-badge']",
+ ) as HTMLElement;
expect(statusBadge.textContent).toBe("Disabled");
});
@@ -619,17 +673,22 @@ describe("TOTP Settings", () => {
// Navigate through enable flow
const enableBtn = container.querySelector("[data-testid='totp-enable-btn']") as HTMLElement;
enableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
- const submitBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Submit") as HTMLElement;
+ const submitBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Submit",
+ ) as HTMLElement;
submitBtn.click();
await vi.waitFor(() => {
expect(container.querySelector("[data-testid='totp-qr-uri']")).not.toBeNull();
});
- const codeInput = container.querySelector("[data-testid='totp-code-input']") as HTMLInputElement;
+ const codeInput = container.querySelector(
+ "[data-testid='totp-code-input']",
+ ) as HTMLInputElement;
codeInput.value = "123456";
(updateUser as ReturnType).mockClear();
@@ -658,12 +717,15 @@ describe("TOTP Settings", () => {
const disableBtn = container.querySelector("[data-testid='totp-disable-btn']") as HTMLElement;
disableBtn.click();
- const pwInput = container.querySelector("[data-testid='totp-password-input']") as HTMLInputElement;
+ const pwInput = container.querySelector(
+ "[data-testid='totp-password-input']",
+ ) as HTMLInputElement;
pwInput.value = "mypassword123";
(updateUser as ReturnType).mockClear();
- const confirmBtn = Array.from(container.querySelectorAll(".ac-btn"))
- .find((b) => b.textContent === "Confirm Disable") as HTMLElement;
+ const confirmBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
+ (b) => b.textContent === "Confirm Disable",
+ ) as HTMLElement;
confirmBtn.click();
await vi.waitFor(() => {
diff --git a/Client/tauri-client/tests/unit/types.test.ts b/Client/tauri-client/tests/unit/types.test.ts
index 09a5acb9..29d9b5e4 100644
--- a/Client/tauri-client/tests/unit/types.test.ts
+++ b/Client/tauri-client/tests/unit/types.test.ts
@@ -25,21 +25,27 @@ const sampleReady = {
payload: {
channels: [
{
- id: 1, name: "general", type: "text" as const,
- category: "Main", position: 0, unread_count: 3, last_message_id: 1040,
+ id: 1,
+ name: "general",
+ type: "text" as const,
+ category: "Main",
+ position: 0,
+ unread_count: 3,
+ last_message_id: 1040,
},
{
- id: 10, name: "voice-chat", type: "voice" as const,
- category: "Main", position: 1,
+ id: 10,
+ name: "voice-chat",
+ type: "voice" as const,
+ category: "Main",
+ position: 1,
},
],
members: [
{ id: 1, username: "alex", avatar: "uuid.png", role: "admin", status: "online" as const },
{ id: 2, username: "jordan", avatar: null, role: "member", status: "idle" as const },
],
- voice_states: [
- { channel_id: 10, user_id: 2, muted: false, deafened: false },
- ],
+ voice_states: [{ channel_id: 10, user_id: 2, muted: false, deafened: false }],
roles: [
{ id: 1, name: "Owner", color: "#E74C3C", permissions: 2147483647 },
{ id: 2, name: "Admin", color: "#F39C12", permissions: 1073741823 },
@@ -51,14 +57,20 @@ const sampleReady = {
const sampleChatMessage = {
type: "chat_message" as const,
payload: {
- id: 1042, channel_id: 5,
+ id: 1042,
+ channel_id: 5,
user: { id: 1, username: "alex", avatar: "uuid.png" },
content: "Hello everyone!",
reply_to: null,
- attachments: [{
- id: "upload-uuid-1", filename: "photo.jpg",
- size: 204800, mime: "image/jpeg", url: "/files/upload-uuid-1",
- }],
+ attachments: [
+ {
+ id: "upload-uuid-1",
+ filename: "photo.jpg",
+ size: 204800,
+ mime: "image/jpeg",
+ url: "/files/upload-uuid-1",
+ },
+ ],
timestamp: "2026-03-14T10:30:00Z",
},
};
@@ -66,9 +78,13 @@ const sampleChatMessage = {
const sampleVoiceConfig = {
type: "voice_config" as const,
payload: {
- channel_id: 10, quality: "medium" as const, bitrate: 64000,
- threshold_mode: "forwarding" as const, mixing_threshold: 10,
- top_speakers: 3, max_users: 50,
+ channel_id: 10,
+ quality: "medium" as const,
+ bitrate: 64000,
+ threshold_mode: "forwarding" as const,
+ mixing_threshold: 10,
+ top_speakers: 3,
+ max_users: 50,
},
};
@@ -153,13 +169,32 @@ describe("AUDIT Critical: no channel_focus message type", () => {
// This test documents that channel_focus is intentionally excluded.
// If someone accidentally adds it, this comment serves as a warning.
const validTypes = [
- "auth_ok", "auth_error", "ready", "chat_message", "chat_send_ok",
- "chat_edited", "chat_deleted", "reaction_update", "typing", "presence",
- "channel_create", "channel_update", "channel_delete",
- "voice_state", "voice_leave", "voice_config", "voice_speakers",
- "voice_offer", "voice_answer", "voice_ice",
- "member_join", "member_leave", "member_update", "member_ban",
- "server_restart", "error",
+ "auth_ok",
+ "auth_error",
+ "ready",
+ "chat_message",
+ "chat_send_ok",
+ "chat_edited",
+ "chat_deleted",
+ "reaction_update",
+ "typing",
+ "presence",
+ "channel_create",
+ "channel_update",
+ "channel_delete",
+ "voice_state",
+ "voice_leave",
+ "voice_config",
+ "voice_speakers",
+ "voice_offer",
+ "voice_answer",
+ "voice_ice",
+ "member_join",
+ "member_leave",
+ "member_update",
+ "member_ban",
+ "server_restart",
+ "error",
];
expect(validTypes).not.toContain("channel_focus");
});
@@ -219,7 +254,7 @@ describe("Permission bitfield", () => {
});
it("ADMINISTRATOR bit can be checked with bitwise AND", () => {
- const ownerPerms = 0x7FFFFFFF;
+ const ownerPerms = 0x7fffffff;
expect(ownerPerms & P.ADMINISTRATOR).toBeTruthy();
const memberPerms = 0x00000663;
diff --git a/Client/tauri-client/tests/unit/ui.store.test.ts b/Client/tauri-client/tests/unit/ui.store.test.ts
index 1b6513a9..018903a9 100644
--- a/Client/tauri-client/tests/unit/ui.store.test.ts
+++ b/Client/tauri-client/tests/unit/ui.store.test.ts
@@ -312,28 +312,19 @@ describe("ui store", () => {
});
it("resets to empty set for invalid JSON", () => {
- localStorage.setItem(
- "owncord:collapsed:test-server:443",
- "not-json",
- );
+ localStorage.setItem("owncord:collapsed:test-server:443", "not-json");
loadCollapsedCategories("test-server:443");
expect(uiStore.getState().collapsedCategories.size).toBe(0);
});
it("resets to empty set for non-array data", () => {
- localStorage.setItem(
- "owncord:collapsed:test-server:443",
- JSON.stringify({ foo: "bar" }),
- );
+ localStorage.setItem("owncord:collapsed:test-server:443", JSON.stringify({ foo: "bar" }));
loadCollapsedCategories("test-server:443");
expect(uiStore.getState().collapsedCategories.size).toBe(0);
});
it("resets to empty set for array with non-string elements", () => {
- localStorage.setItem(
- "owncord:collapsed:test-server:443",
- JSON.stringify([1, 2, 3]),
- );
+ localStorage.setItem("owncord:collapsed:test-server:443", JSON.stringify([1, 2, 3]));
loadCollapsedCategories("test-server:443");
expect(uiStore.getState().collapsedCategories.size).toBe(0);
});
diff --git a/Client/tauri-client/tests/unit/user-bar.test.ts b/Client/tauri-client/tests/unit/user-bar.test.ts
index 16a57c82..2c3b08ae 100644
--- a/Client/tauri-client/tests/unit/user-bar.test.ts
+++ b/Client/tauri-client/tests/unit/user-bar.test.ts
@@ -8,15 +8,10 @@ vi.mock("@stores/ui.store", () => ({
uiStore: { getState: () => ({}), subscribe: () => () => {} },
}));
-function setAuthState(
- user: { username: string } | null,
- isAuthenticated: boolean,
-): void {
+function setAuthState(user: { username: string } | null, isAuthenticated: boolean): void {
authStore.setState(() => ({
token: isAuthenticated ? "tok" : null,
- user: user !== null
- ? { id: 1, username: user.username, avatar: null, role: "member" }
- : null,
+ user: user !== null ? { id: 1, username: user.username, avatar: null, role: "member" } : null,
serverName: "TestServer",
motd: null,
isAuthenticated,
@@ -128,7 +123,9 @@ describe("UserBar", () => {
comp = createUserBar({ onDisconnect });
comp.mount(container);
- const disconnectBtn = container.querySelector('[data-testid="disconnect-btn"]') as HTMLButtonElement;
+ const disconnectBtn = container.querySelector(
+ '[data-testid="disconnect-btn"]',
+ ) as HTMLButtonElement;
expect(disconnectBtn).not.toBeNull();
expect(disconnectBtn.getAttribute("aria-label")).toBe("Switch server");
});
@@ -139,7 +136,9 @@ describe("UserBar", () => {
comp = createUserBar({ onDisconnect });
comp.mount(container);
- const disconnectBtn = container.querySelector('[data-testid="disconnect-btn"]') as HTMLButtonElement;
+ const disconnectBtn = container.querySelector(
+ '[data-testid="disconnect-btn"]',
+ ) as HTMLButtonElement;
disconnectBtn.click();
expect(onDisconnect).toHaveBeenCalledOnce();
});
diff --git a/Client/tauri-client/tests/unit/video-grid.test.ts b/Client/tauri-client/tests/unit/video-grid.test.ts
index c782489b..ed02764e 100644
--- a/Client/tauri-client/tests/unit/video-grid.test.ts
+++ b/Client/tauri-client/tests/unit/video-grid.test.ts
@@ -53,9 +53,15 @@ describe("VideoGrid", () => {
vi.clearAllMocks();
// ResizeObserver is not available in JSDOM
globalThis.ResizeObserver ??= class {
- observe(): void { /* noop */ }
- unobserve(): void { /* noop */ }
- disconnect(): void { /* noop */ }
+ observe(): void {
+ /* noop */
+ }
+ unobserve(): void {
+ /* noop */
+ }
+ disconnect(): void {
+ /* noop */
+ }
} as unknown as typeof ResizeObserver;
container = document.createElement("div");
diff --git a/Client/tauri-client/tests/unit/video-mode-controller.test.ts b/Client/tauri-client/tests/unit/video-mode-controller.test.ts
index bd3b2609..f7accb6e 100644
--- a/Client/tauri-client/tests/unit/video-mode-controller.test.ts
+++ b/Client/tauri-client/tests/unit/video-mode-controller.test.ts
@@ -4,11 +4,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
// Mocks
// ---------------------------------------------------------------------------
-const { mockVoiceStoreGetState, mockGetLocalCameraStream, mockGetLocalScreenshareStream } = vi.hoisted(() => ({
- mockVoiceStoreGetState: vi.fn(),
- mockGetLocalCameraStream: vi.fn((): MediaStream | null => null),
- mockGetLocalScreenshareStream: vi.fn((): MediaStream | null => null),
-}));
+const { mockVoiceStoreGetState, mockGetLocalCameraStream, mockGetLocalScreenshareStream } =
+ vi.hoisted(() => ({
+ mockVoiceStoreGetState: vi.fn(),
+ mockGetLocalCameraStream: vi.fn((): MediaStream | null => null),
+ mockGetLocalScreenshareStream: vi.fn((): MediaStream | null => null),
+ }));
vi.mock("@stores/voice.store", () => ({
voiceStore: { getState: mockVoiceStoreGetState },
@@ -54,7 +55,10 @@ interface VoiceStateStub {
currentChannelId: number | null;
localCamera: boolean;
localScreenshare: boolean;
- voiceUsers: Map>;
+ voiceUsers: Map<
+ number,
+ Map
+ >;
}
function makeVoiceState(overrides: Partial = {}): VoiceStateStub {
@@ -181,7 +185,11 @@ describe("createVideoModeController", () => {
});
ctrl.checkVideoMode();
- expect(vg.addStream).toHaveBeenCalledWith(1, "me (You)", fakeStream, { isSelf: true, audioUserId: 1, isScreenshare: false });
+ expect(vg.addStream).toHaveBeenCalledWith(1, "me (You)", fakeStream, {
+ isSelf: true,
+ audioUserId: 1,
+ isScreenshare: false,
+ });
});
it("removes local tile when local camera is off", () => {
@@ -305,7 +313,11 @@ describe("createVideoModeController", () => {
ctrl.checkVideoMode();
// screenshareUserId = currentUserId + 1_000_000 = 1 + 1_000_000 = 1_000_001
- expect(vg.addStream).toHaveBeenCalledWith(1_000_001, "me (Screen)", fakeStream, { isSelf: true, audioUserId: 1, isScreenshare: true });
+ expect(vg.addStream).toHaveBeenCalledWith(1_000_001, "me (Screen)", fakeStream, {
+ isSelf: true,
+ audioUserId: 1,
+ isScreenshare: true,
+ });
});
it("removes local screenshare tile when screenshare is turned off", () => {
@@ -313,7 +325,11 @@ describe("createVideoModeController", () => {
// First call: screenshare on — tile added
mockVoiceStoreGetState.mockReturnValue(
- makeVoiceState({ currentChannelId: 10, localScreenshare: true, voiceUsers: new Map([[10, users]]) }),
+ makeVoiceState({
+ currentChannelId: 10,
+ localScreenshare: true,
+ voiceUsers: new Map([[10, users]]),
+ }),
);
const fakeStream = { getTracks: () => [] } as unknown as MediaStream;
mockGetLocalScreenshareStream.mockReturnValue(fakeStream);
@@ -325,11 +341,19 @@ describe("createVideoModeController", () => {
getCurrentUserId: () => 1,
});
ctrl.checkVideoMode();
- expect(vg.addStream).toHaveBeenCalledWith(1_000_001, "me (Screen)", fakeStream, { isSelf: true, audioUserId: 1, isScreenshare: true });
+ expect(vg.addStream).toHaveBeenCalledWith(1_000_001, "me (Screen)", fakeStream, {
+ isSelf: true,
+ audioUserId: 1,
+ isScreenshare: true,
+ });
// Second call: screenshare off — tile removed
mockVoiceStoreGetState.mockReturnValue(
- makeVoiceState({ currentChannelId: 10, localScreenshare: false, voiceUsers: new Map([[10, users]]) }),
+ makeVoiceState({
+ currentChannelId: 10,
+ localScreenshare: false,
+ voiceUsers: new Map([[10, users]]),
+ }),
);
ctrl.checkVideoMode();
expect(vg.removeStream).toHaveBeenCalledWith(1_000_001);
diff --git a/Client/tauri-client/tests/unit/voice-audio-tab.test.ts b/Client/tauri-client/tests/unit/voice-audio-tab.test.ts
index 61db5b8c..d9753185 100644
--- a/Client/tauri-client/tests/unit/voice-audio-tab.test.ts
+++ b/Client/tauri-client/tests/unit/voice-audio-tab.test.ts
@@ -24,24 +24,27 @@ describe("VoiceAudioTab camera preview", () => {
document.body.innerHTML = "";
localStorage.setItem("owncord:settings:videoInputDevice", '"camera-1"');
- vi.stubGlobal("AudioContext", class {
- createAnalyser() {
- return {
- fftSize: 0,
- smoothingTimeConstant: 0,
- frequencyBinCount: 32,
- getByteFrequencyData: vi.fn(),
- };
- }
+ vi.stubGlobal(
+ "AudioContext",
+ class {
+ createAnalyser() {
+ return {
+ fftSize: 0,
+ smoothingTimeConstant: 0,
+ frequencyBinCount: 32,
+ getByteFrequencyData: vi.fn(),
+ };
+ }
- createMediaStreamSource() {
- return { connect: vi.fn() };
- }
+ createMediaStreamSource() {
+ return { connect: vi.fn() };
+ }
- close() {
- return Promise.resolve();
- }
- });
+ close() {
+ return Promise.resolve();
+ }
+ },
+ );
});
it("does not restore a stale camera stream after the tab is aborted", async () => {
@@ -56,9 +59,9 @@ describe("VoiceAudioTab camera preview", () => {
vi.stubGlobal("navigator", {
mediaDevices: {
- enumerateDevices: vi.fn().mockResolvedValue([
- { kind: "videoinput", deviceId: "camera-1", label: "Camera 1" },
- ]),
+ enumerateDevices: vi
+ .fn()
+ .mockResolvedValue([{ kind: "videoinput", deviceId: "camera-1", label: "Camera 1" }]),
getUserMedia: vi.fn().mockImplementation((constraints: MediaStreamConstraints) => {
if (constraints.video && constraints.audio === false) {
return new Promise((resolve) => {
@@ -97,9 +100,9 @@ describe("VoiceAudioTab camera preview", () => {
vi.stubGlobal("navigator", {
mediaDevices: {
- enumerateDevices: vi.fn().mockResolvedValue([
- { kind: "videoinput", deviceId: "camera-1", label: "Camera 1" },
- ]),
+ enumerateDevices: vi
+ .fn()
+ .mockResolvedValue([{ kind: "videoinput", deviceId: "camera-1", label: "Camera 1" }]),
getUserMedia: vi.fn().mockImplementation((constraints: MediaStreamConstraints) => {
if (constraints.video && constraints.audio === false) {
return new Promise((resolve) => {
@@ -132,7 +135,9 @@ describe("VoiceAudioTab camera preview", () => {
// ---------------------------------------------------------------------------
describe("VoiceAudioTab UI structure", () => {
- function stubNavigator(devices: Array<{ kind: string; deviceId: string; label: string }> = []): void {
+ function stubNavigator(
+ devices: Array<{ kind: string; deviceId: string; label: string }> = [],
+ ): void {
const audioStream = {
getTracks: () => [{ stop: vi.fn() }],
} as unknown as MediaStream;
@@ -149,16 +154,25 @@ describe("VoiceAudioTab UI structure", () => {
vi.clearAllMocks();
localStorage.clear();
document.body.innerHTML = "";
- vi.stubGlobal("AudioContext", class {
- createAnalyser() {
- return {
- fftSize: 0, smoothingTimeConstant: 0, frequencyBinCount: 32,
- getByteFrequencyData: vi.fn(),
- };
- }
- createMediaStreamSource() { return { connect: vi.fn() }; }
- close() { return Promise.resolve(); }
- });
+ vi.stubGlobal(
+ "AudioContext",
+ class {
+ createAnalyser() {
+ return {
+ fftSize: 0,
+ smoothingTimeConstant: 0,
+ frequencyBinCount: 32,
+ getByteFrequencyData: vi.fn(),
+ };
+ }
+ createMediaStreamSource() {
+ return { connect: vi.fn() };
+ }
+ close() {
+ return Promise.resolve();
+ }
+ },
+ );
});
afterEach(() => {
@@ -287,9 +301,7 @@ describe("VoiceAudioTab UI structure", () => {
});
it("input device change calls switchInputDevice and saves pref", async () => {
- stubNavigator([
- { kind: "audioinput", deviceId: "mic-1", label: "Mic 1" },
- ]);
+ stubNavigator([{ kind: "audioinput", deviceId: "mic-1", label: "Mic 1" }]);
const ac = new AbortController();
const tab = createVoiceAudioTab(ac.signal);
const el = tab.build();
@@ -309,9 +321,7 @@ describe("VoiceAudioTab UI structure", () => {
});
it("output device change calls switchOutputDevice and saves pref", async () => {
- stubNavigator([
- { kind: "audiooutput", deviceId: "spk-1", label: "Speaker 1" },
- ]);
+ stubNavigator([{ kind: "audiooutput", deviceId: "spk-1", label: "Speaker 1" }]);
const ac = new AbortController();
const tab = createVoiceAudioTab(ac.signal);
const el = tab.build();
@@ -393,8 +403,8 @@ describe("VoiceAudioTab UI structure", () => {
const inputSelect = el.querySelectorAll("select")[0]!;
const options = inputSelect.querySelectorAll("option");
// Should have default + error option
- const texts = Array.from(options).map(o => o.textContent);
- expect(texts.some(t => t?.includes("Could not enumerate"))).toBe(true);
+ const texts = Array.from(options).map((o) => o.textContent);
+ expect(texts.some((t) => t?.includes("Could not enumerate"))).toBe(true);
});
ac.abort();
@@ -425,9 +435,9 @@ describe("VoiceAudioTab UI structure", () => {
vi.stubGlobal("navigator", {
mediaDevices: {
- enumerateDevices: vi.fn().mockResolvedValue([
- { kind: "videoinput", deviceId: "cam-1", label: "Camera 1" },
- ]),
+ enumerateDevices: vi
+ .fn()
+ .mockResolvedValue([{ kind: "videoinput", deviceId: "cam-1", label: "Camera 1" }]),
getUserMedia: vi.fn().mockImplementation((constraints: MediaStreamConstraints) => {
if (constraints.video && constraints.audio === false) {
return Promise.resolve(cameraStream);
@@ -459,9 +469,9 @@ describe("VoiceAudioTab UI structure", () => {
vi.stubGlobal("navigator", {
mediaDevices: {
- enumerateDevices: vi.fn().mockResolvedValue([
- { kind: "videoinput", deviceId: "cam-1", label: "Camera 1" },
- ]),
+ enumerateDevices: vi
+ .fn()
+ .mockResolvedValue([{ kind: "videoinput", deviceId: "cam-1", label: "Camera 1" }]),
getUserMedia: vi.fn().mockImplementation((constraints: MediaStreamConstraints) => {
if (constraints.video && constraints.audio === false) {
return Promise.resolve(cameraStream);
@@ -502,9 +512,9 @@ describe("VoiceAudioTab UI structure", () => {
vi.stubGlobal("navigator", {
mediaDevices: {
- enumerateDevices: vi.fn().mockResolvedValue([
- { kind: "videoinput", deviceId: "cam-1", label: "Camera 1" },
- ]),
+ enumerateDevices: vi
+ .fn()
+ .mockResolvedValue([{ kind: "videoinput", deviceId: "cam-1", label: "Camera 1" }]),
getUserMedia: vi.fn().mockImplementation((constraints: MediaStreamConstraints) => {
if (constraints.video && constraints.audio === false) {
return Promise.reject(new Error("Camera access denied"));
@@ -575,7 +585,7 @@ describe("VoiceAudioTab UI structure", () => {
document.body.appendChild(el);
// Should not throw — mic meter stays empty
- await new Promise(r => setTimeout(r, 0));
+ await new Promise((r) => setTimeout(r, 0));
ac.abort();
});
@@ -601,9 +611,7 @@ describe("VoiceAudioTab UI structure", () => {
});
it("uses device ID fallback label for devices without labels", async () => {
- stubNavigator([
- { kind: "audioinput", deviceId: "abcdef12", label: "" },
- ]);
+ stubNavigator([{ kind: "audioinput", deviceId: "abcdef12", label: "" }]);
const ac = new AbortController();
const tab = createVoiceAudioTab(ac.signal);
const el = tab.build();
@@ -664,4 +672,4 @@ describe("VoiceAudioTab UI structure", () => {
expect(() => tab.build()).not.toThrow();
ac.abort();
});
-});
\ No newline at end of file
+});
diff --git a/Client/tauri-client/tests/unit/voice-channel.test.ts b/Client/tauri-client/tests/unit/voice-channel.test.ts
index 45ac0acb..8b93f13c 100644
--- a/Client/tauri-client/tests/unit/voice-channel.test.ts
+++ b/Client/tauri-client/tests/unit/voice-channel.test.ts
@@ -421,9 +421,7 @@ describe("VoiceChannel", () => {
const userItems = result.element.querySelectorAll(".voice-user-item");
expect(userItems.length).toBe(2);
- const names = Array.from(userItems).map(
- (el) => el.querySelector(".vu-name")?.textContent,
- );
+ const names = Array.from(userItems).map((el) => el.querySelector(".vu-name")?.textContent);
expect(names).toContain("Alice");
expect(names).toContain("Bob");
@@ -573,11 +571,13 @@ describe("VoiceChannel", () => {
container.appendChild(result.element);
const userRow = result.element.querySelector(".voice-user-item") as HTMLElement;
- userRow.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 200,
- clientY: 300,
- }));
+ userRow.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 200,
+ clientY: 300,
+ }),
+ );
// Menu should be appended to document.body
const menu = document.body.querySelector(".context-menu");
@@ -629,11 +629,13 @@ describe("VoiceChannel", () => {
container.appendChild(result.element);
const userRow = result.element.querySelector(".voice-user-item") as HTMLElement;
- userRow.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 200,
- clientY: 300,
- }));
+ userRow.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 200,
+ clientY: 300,
+ }),
+ );
// No context menu should appear
const menu = document.body.querySelector(".context-menu");
@@ -675,11 +677,13 @@ describe("VoiceChannel", () => {
// Open context menu
const userRow = result.element.querySelector(".voice-user-item") as HTMLElement;
- userRow.dispatchEvent(new MouseEvent("contextmenu", {
- bubbles: true,
- clientX: 200,
- clientY: 300,
- }));
+ userRow.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ bubbles: true,
+ clientX: 200,
+ clientY: 300,
+ }),
+ );
expect(document.body.querySelector(".context-menu")).not.toBeNull();
diff --git a/Client/tauri-client/tests/unit/voice-disconnect.test.ts b/Client/tauri-client/tests/unit/voice-disconnect.test.ts
index bfa97f1a..44d40a10 100644
--- a/Client/tauri-client/tests/unit/voice-disconnect.test.ts
+++ b/Client/tauri-client/tests/unit/voice-disconnect.test.ts
@@ -120,8 +120,12 @@ describe("Voice disconnect — logout cleanup", () => {
const wsDisconnect = vi.fn();
const callOrder: string[] = [];
- wsSend.mockImplementation(() => { callOrder.push("send"); });
- wsDisconnect.mockImplementation(() => { callOrder.push("disconnect"); });
+ wsSend.mockImplementation(() => {
+ callOrder.push("send");
+ });
+ wsDisconnect.mockImplementation(() => {
+ callOrder.push("disconnect");
+ });
// User is authenticated and in a voice channel
authStore.setState((prev) => ({
diff --git a/Client/tauri-client/tests/unit/voice-widget.test.ts b/Client/tauri-client/tests/unit/voice-widget.test.ts
index d55d2689..8338789a 100644
--- a/Client/tauri-client/tests/unit/voice-widget.test.ts
+++ b/Client/tauri-client/tests/unit/voice-widget.test.ts
@@ -16,8 +16,14 @@ vi.mock("@lib/connectionStats", () => ({
start: vi.fn(),
stop: vi.fn(),
getStats: vi.fn().mockReturnValue({
- rtt: 0, quality: "excellent", outRate: 0, inRate: 0,
- outPackets: 0, inPackets: 0, totalUp: 0, totalDown: 0,
+ rtt: 0,
+ quality: "excellent",
+ outRate: 0,
+ inRate: 0,
+ outPackets: 0,
+ inPackets: 0,
+ totalUp: 0,
+ totalDown: 0,
}),
onUpdate: vi.fn().mockReturnValue(() => {}),
onQualityChanged: vi.fn().mockReturnValue(() => {}),
@@ -281,7 +287,9 @@ describe("VoiceWidget", () => {
});
widget.mount(container);
- const screenshareBtn = container.querySelector('[aria-label="Screenshare"]') as HTMLButtonElement;
+ const screenshareBtn = container.querySelector(
+ '[aria-label="Screenshare"]',
+ ) as HTMLButtonElement;
expect(screenshareBtn).not.toBeNull();
expect(screenshareBtn.classList.contains("active-ctrl")).toBe(true);
expect(screenshareBtn.getAttribute("aria-pressed")).toBe("true");
diff --git a/Client/tauri-client/tests/unit/voice.store.test.ts b/Client/tauri-client/tests/unit/voice.store.test.ts
index 4a321331..61fe6e10 100644
--- a/Client/tauri-client/tests/unit/voice.store.test.ts
+++ b/Client/tauri-client/tests/unit/voice.store.test.ts
@@ -17,11 +17,7 @@ import {
setVoiceConfig,
getChannelVoiceUsers,
} from "../../src/stores/voice.store";
-import type {
- ReadyVoiceState,
- VoiceStatePayload,
- VoiceLeavePayload,
-} from "../../src/lib/types";
+import type { ReadyVoiceState, VoiceStatePayload, VoiceLeavePayload } from "../../src/lib/types";
import { authStore } from "../../src/stores/auth.store";
function resetStore(): void {
@@ -503,7 +499,11 @@ describe("voice store", () => {
expect(ch10?.has(2)).toBe(true);
authStore.setState(() => ({
- token: null, user: null, serverName: null, motd: null, isAuthenticated: false,
+ token: null,
+ user: null,
+ serverName: null,
+ motd: null,
+ isAuthenticated: false,
}));
});
@@ -523,7 +523,11 @@ describe("voice store", () => {
expect(voiceStore.getState().voiceUsers.has(20)).toBe(false);
authStore.setState(() => ({
- token: null, user: null, serverName: null, motd: null, isAuthenticated: false,
+ token: null,
+ user: null,
+ serverName: null,
+ motd: null,
+ isAuthenticated: false,
}));
});
});
@@ -544,7 +548,11 @@ describe("voice store", () => {
expect(voiceStore.getState().currentChannelId).toBe(10);
authStore.setState(() => ({
- token: null, user: null, serverName: null, motd: null, isAuthenticated: false,
+ token: null,
+ user: null,
+ serverName: null,
+ motd: null,
+ isAuthenticated: false,
}));
});
@@ -564,7 +572,11 @@ describe("voice store", () => {
expect(voiceStore.getState().currentChannelId).toBe(42);
authStore.setState(() => ({
- token: null, user: null, serverName: null, motd: null, isAuthenticated: false,
+ token: null,
+ user: null,
+ serverName: null,
+ motd: null,
+ isAuthenticated: false,
}));
});
});
diff --git a/Client/tauri-client/tests/unit/ws.test.ts b/Client/tauri-client/tests/unit/ws.test.ts
index ec3dd0bd..adeea521 100644
--- a/Client/tauri-client/tests/unit/ws.test.ts
+++ b/Client/tauri-client/tests/unit/ws.test.ts
@@ -114,14 +114,17 @@ describe("WebSocket Client (Tauri proxy)", () => {
const states: ConnectionState[] = [];
client.onStateChange((s) => states.push(s));
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok",
- payload: {
- user: { id: 1, username: "alex", avatar: null, role: "admin" },
- server_name: "Test",
- motd: "Hello",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ payload: {
+ user: { id: 1, username: "alex", avatar: null, role: "admin" },
+ server_name: "Test",
+ motd: "Hello",
+ },
+ }),
+ );
expect(states).toContain("connected");
});
@@ -134,16 +137,21 @@ describe("WebSocket Client (Tauri proxy)", () => {
const messages: unknown[] = [];
client.on("chat_message", (payload) => messages.push(payload));
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message",
- payload: {
- id: 1, channel_id: 5,
- user: { id: 1, username: "alex", avatar: null },
- content: "Hello",
- reply_to: null, attachments: [],
- timestamp: "2026-03-14T10:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ payload: {
+ id: 1,
+ channel_id: 5,
+ user: { id: 1, username: "alex", avatar: null },
+ content: "Hello",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-03-14T10:00:00Z",
+ },
+ }),
+ );
expect(messages).toHaveLength(1);
});
@@ -157,16 +165,21 @@ describe("WebSocket Client (Tauri proxy)", () => {
const unsub = client.on("chat_message", (payload) => messages.push(payload));
unsub();
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message",
- payload: {
- id: 1, channel_id: 5,
- user: { id: 1, username: "alex", avatar: null },
- content: "Hello",
- reply_to: null, attachments: [],
- timestamp: "2026-03-14T10:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ payload: {
+ id: 1,
+ channel_id: 5,
+ user: { id: 1, username: "alex", avatar: null },
+ content: "Hello",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-03-14T10:00:00Z",
+ },
+ }),
+ );
expect(messages).toHaveLength(0);
});
@@ -179,10 +192,13 @@ describe("WebSocket Client (Tauri proxy)", () => {
const authErrors: unknown[] = [];
client.on("auth_error", (payload) => authErrors.push(payload));
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_error",
- payload: { message: "Invalid token" },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_error",
+ payload: { message: "Invalid token" },
+ }),
+ );
await vi.advanceTimersByTimeAsync(60_000);
@@ -195,13 +211,17 @@ describe("WebSocket Client (Tauri proxy)", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok",
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
const states: ConnectionState[] = [];
client.onStateChange((s) => states.push(s));
@@ -245,10 +265,12 @@ describe("WebSocket Client (Tauri proxy)", () => {
const bigData = JSON.stringify({
type: "chat_message",
payload: {
- id: 1, channel_id: 1,
+ id: 1,
+ channel_id: 1,
user: { id: 1, username: "a", avatar: null },
content: "x".repeat(100),
- reply_to: null, attachments: [],
+ reply_to: null,
+ attachments: [],
timestamp: "2026-01-01T00:00:00Z",
},
});
@@ -305,7 +327,10 @@ describe("lastSeq tracking", () => {
emitTauriEvent("ws-state", "open");
const authCall = mockInvoke.mock.calls.find(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"auth"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"auth"'),
);
expect(authCall).toBeDefined();
const authMsg = JSON.parse((authCall![1] as { message: string }).message);
@@ -318,26 +343,36 @@ describe("lastSeq tracking", () => {
emitTauriEvent("ws-state", "open");
// Send auth_ok so we're connected
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok",
- seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Send a message with seq 42
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message",
- seq: 42,
- payload: {
- id: 1, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "hi", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ seq: 42,
+ payload: {
+ id: 1,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "hi",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
// Now simulate a disconnect + reconnect to verify lastSeq was updated
emitTauriEvent("ws-state", "closed");
@@ -347,7 +382,10 @@ describe("lastSeq tracking", () => {
emitTauriEvent("ws-state", "open");
const authCall = mockInvoke.mock.calls.find(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"auth"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"auth"'),
);
expect(authCall).toBeDefined();
const authMsg = JSON.parse((authCall![1] as { message: string }).message);
@@ -359,14 +397,18 @@ describe("lastSeq tracking", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok",
- seq: 5,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 5,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Disconnect unexpectedly
emitTauriEvent("ws-state", "closed");
@@ -376,7 +418,10 @@ describe("lastSeq tracking", () => {
emitTauriEvent("ws-state", "open");
const authCall = mockInvoke.mock.calls.find(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"auth"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"auth"'),
);
expect(authCall).toBeDefined();
const authMsg = JSON.parse((authCall![1] as { message: string }).message);
@@ -388,13 +433,18 @@ describe("lastSeq tracking", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 10,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 10,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// First auto-reconnect
emitTauriEvent("ws-state", "closed");
@@ -402,22 +452,34 @@ describe("lastSeq tracking", () => {
emitTauriEvent("ws-state", "open");
// Receive more messages with higher seq
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 11,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message", seq: 25,
- payload: {
- id: 2, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "hello", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 11,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ seq: 25,
+ payload: {
+ id: 2,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "hello",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
// Second auto-reconnect
emitTauriEvent("ws-state", "closed");
@@ -426,7 +488,10 @@ describe("lastSeq tracking", () => {
emitTauriEvent("ws-state", "open");
const authCall = mockInvoke.mock.calls.find(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"auth"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"auth"'),
);
const authMsg = JSON.parse((authCall![1] as { message: string }).message);
expect(authMsg.payload.last_seq).toBe(25);
@@ -437,13 +502,18 @@ describe("lastSeq tracking", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 50,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 50,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Intentional disconnect (e.g. logout)
client.disconnect();
@@ -455,7 +525,10 @@ describe("lastSeq tracking", () => {
emitTauriEvent("ws-state", "open");
const authCall = mockInvoke.mock.calls.find(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"auth"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"auth"'),
);
expect(authCall).toBeDefined();
const authMsg = JSON.parse((authCall![1] as { message: string }).message);
@@ -485,13 +558,18 @@ describe("cert mismatch blocking", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Cert mismatch event fires
emitTauriEvent("cert-tofu", {
@@ -509,9 +587,7 @@ describe("cert mismatch blocking", () => {
// Wait well beyond normal backoff — should NOT reconnect
mockInvoke.mockClear();
await vi.advanceTimersByTimeAsync(60_000);
- const reconnectCalls = mockInvoke.mock.calls.filter(
- (c) => c[0] === "ws_connect",
- );
+ const reconnectCalls = mockInvoke.mock.calls.filter((c) => c[0] === "ws_connect");
expect(reconnectCalls).toHaveLength(0);
});
@@ -520,13 +596,18 @@ describe("cert mismatch blocking", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
emitTauriEvent("cert-tofu", {
host: "localhost:8443",
@@ -556,13 +637,18 @@ describe("cert mismatch blocking", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Trigger mismatch
emitTauriEvent("cert-tofu", {
@@ -647,34 +733,53 @@ describe("message handling edge cases", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 10,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 10,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// seq=50 then seq=30 — should keep 50
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message", seq: 50,
- payload: {
- id: 1, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "hi", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ seq: 50,
+ payload: {
+ id: 1,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "hi",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message", seq: 30,
- payload: {
- id: 2, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "hello", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ seq: 30,
+ payload: {
+ id: 2,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "hello",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
// Disconnect and reconnect to verify lastSeq
emitTauriEvent("ws-state", "closed");
@@ -683,7 +788,10 @@ describe("message handling edge cases", () => {
emitTauriEvent("ws-state", "open");
const authCall = mockInvoke.mock.calls.find(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"auth"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"auth"'),
);
const authMsg = JSON.parse((authCall![1] as { message: string }).message);
expect(authMsg.payload.last_seq).toBe(50);
@@ -697,16 +805,22 @@ describe("message handling edge cases", () => {
const messages: unknown[] = [];
client.on("chat_message", (p) => messages.push(p));
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message",
- // no seq field
- payload: {
- id: 1, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "no seq", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ // no seq field
+ payload: {
+ id: 1,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "no seq",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
expect(messages).toHaveLength(1);
});
@@ -717,13 +831,17 @@ describe("message handling edge cases", () => {
emitTauriEvent("ws-state", "open");
// Send a message with no listener registered — should log "no listeners"
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok",
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// No crash means the "no listeners" debug log path executed
expect(client.getState()).toBe("connected");
@@ -743,15 +861,21 @@ describe("message handling edge cases", () => {
const messages: unknown[] = [];
client.on("chat_message", (p) => messages.push(p));
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message",
- payload: {
- id: 1, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "test", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ payload: {
+ id: 1,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "test",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
// Second listener should still receive the message
expect(messages).toHaveLength(1);
@@ -847,23 +971,36 @@ describe("reconnection dedup", () => {
emitTauriEvent("ws-state", "open");
// Auth and get some messages to advance lastSeq
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message", seq: 5, id: "msg-5",
- payload: {
- id: 1, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "original", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ seq: 5,
+ id: "msg-5",
+ payload: {
+ id: 1,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "original",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
// Disconnect unexpectedly
emitTauriEvent("ws-state", "closed");
@@ -879,26 +1016,42 @@ describe("reconnection dedup", () => {
client.on("chat_message", (p) => messages.push(p));
// Send a message during replay -- first occurrence passes
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message", seq: 5, id: "msg-5",
- payload: {
- id: 1, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "original", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ seq: 5,
+ id: "msg-5",
+ payload: {
+ id: 1,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "original",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
// Send the SAME message ID again — should be deduped
- emitTauriEvent("ws-message", JSON.stringify({
- type: "chat_message", seq: 5, id: "msg-5",
- payload: {
- id: 1, channel_id: 1,
- user: { id: 1, username: "a", avatar: null },
- content: "original", reply_to: null, attachments: [],
- timestamp: "2026-01-01T00:00:00Z",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "chat_message",
+ seq: 5,
+ id: "msg-5",
+ payload: {
+ id: 1,
+ channel_id: 1,
+ user: { id: 1, username: "a", avatar: null },
+ content: "original",
+ reply_to: null,
+ attachments: [],
+ timestamp: "2026-01-01T00:00:00Z",
+ },
+ }),
+ );
// Only the first occurrence should pass through
expect(messages).toHaveLength(1);
@@ -910,13 +1063,18 @@ describe("reconnection dedup", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 5,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 5,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Disconnect
emitTauriEvent("ws-state", "closed");
@@ -929,13 +1087,18 @@ describe("reconnection dedup", () => {
client.on("auth_ok", (p) => authPayloads.push(p));
// auth_ok during replay should NOT be deduped
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 6,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 6,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
expect(authPayloads).toHaveLength(1);
// After auth_ok, replay dedup should be cleared
@@ -947,18 +1110,27 @@ describe("reconnection dedup", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
- emitTauriEvent("ws-message", JSON.stringify({
- type: "presence", seq: 10,
- payload: { user_id: 1, status: "idle" },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "presence",
+ seq: 10,
+ payload: { user_id: 1, status: "idle" },
+ }),
+ );
// Disconnect
emitTauriEvent("ws-state", "closed");
@@ -969,22 +1141,34 @@ describe("reconnection dedup", () => {
client.on("presence", (p) => presences.push(p));
// First presence during replay — passes through
- emitTauriEvent("ws-message", JSON.stringify({
- type: "presence", seq: 10,
- payload: { user_id: 1, status: "idle" },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "presence",
+ seq: 10,
+ payload: { user_id: 1, status: "idle" },
+ }),
+ );
// Same type:seq — should be deduped
- emitTauriEvent("ws-message", JSON.stringify({
- type: "presence", seq: 10,
- payload: { user_id: 1, status: "idle" },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "presence",
+ seq: 10,
+ payload: { user_id: 1, status: "idle" },
+ }),
+ );
// Different seq — should pass through
- emitTauriEvent("ws-message", JSON.stringify({
- type: "presence", seq: 11,
- payload: { user_id: 1, status: "online" },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "presence",
+ seq: 11,
+ payload: { user_id: 1, status: "online" },
+ }),
+ );
expect(presences).toHaveLength(2);
expect((presences[0] as { status: string }).status).toBe("idle");
@@ -1023,13 +1207,18 @@ describe("heartbeat", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
mockInvoke.mockClear();
@@ -1037,7 +1226,10 @@ describe("heartbeat", () => {
await vi.advanceTimersByTimeAsync(30_000);
const pingSends = mockInvoke.mock.calls.filter(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"ping"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"ping"'),
);
expect(pingSends.length).toBeGreaterThanOrEqual(1);
});
@@ -1047,13 +1239,18 @@ describe("heartbeat", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
client.disconnect();
mockInvoke.mockClear();
@@ -1062,7 +1259,10 @@ describe("heartbeat", () => {
await vi.advanceTimersByTimeAsync(60_000);
const pingSends = mockInvoke.mock.calls.filter(
- (c) => c[0] === "ws_send" && typeof c[1]?.message === "string" && (c[1].message as string).includes('"type":"ping"'),
+ (c) =>
+ c[0] === "ws_send" &&
+ typeof c[1]?.message === "string" &&
+ (c[1].message as string).includes('"type":"ping"'),
);
expect(pingSends).toHaveLength(0);
});
@@ -1116,13 +1316,18 @@ describe("send edge cases", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Drop connection
emitTauriEvent("ws-state", "closed");
@@ -1131,13 +1336,18 @@ describe("send edge cases", () => {
await vi.advanceTimersByTimeAsync(1100);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 2,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 2,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Drop again
emitTauriEvent("ws-state", "closed");
@@ -1155,13 +1365,18 @@ describe("send edge cases", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Make ws_send reject
mockInvoke.mockImplementation(async (cmd: string) => {
@@ -1185,13 +1400,18 @@ describe("send edge cases", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Make ws_disconnect throw
mockInvoke.mockImplementation(async (cmd: string) => {
@@ -1214,26 +1434,36 @@ describe("send edge cases", () => {
await vi.advanceTimersByTimeAsync(10);
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: 1,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: 1,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
// Force multiple reconnect attempts to ramp up backoff
for (let i = 0; i < 5; i++) {
emitTauriEvent("ws-state", "closed");
await vi.advanceTimersByTimeAsync(10_000); // well past any backoff
emitTauriEvent("ws-state", "open");
- emitTauriEvent("ws-message", JSON.stringify({
- type: "auth_ok", seq: i + 2,
- payload: {
- user: { id: 1, username: "a", avatar: null, role: "admin" },
- server_name: "S", motd: "",
- },
- }));
+ emitTauriEvent(
+ "ws-message",
+ JSON.stringify({
+ type: "auth_ok",
+ seq: i + 2,
+ payload: {
+ user: { id: 1, username: "a", avatar: null, role: "admin" },
+ server_name: "S",
+ motd: "",
+ },
+ }),
+ );
}
// At this point, the reconnect delay should be capped at 5000ms