mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* test(server): admin/handlers/channels — test-audit 2026-08-19 fixes * test(server): api/constants — test-audit 2026-08-19 fixes * test(server): api/middleware — test-audit 2026-08-19 fixes * test(server): api/waf — test-audit 2026-08-19 fixes * test(server): auth/totp/encrypt — test-audit 2026-08-19 fixes * test(server): db/session/expiry/test — test-audit 2026-08-19 fixes * test(server): migrations/030/attachments/unlink/on/message/delete — test-audit 2026-08-19 fixes * test(server): updater/download — test-audit 2026-08-19 fixes * test(server): ws/handlers_command — test-audit 2026-08-19 fixes * test(server): ws/hub/broadcast — test-audit 2026-08-19 fixes * test(server): ws/hub/events — test-audit 2026-08-19 fixes * test(server): ws/livekit/webhook — test-audit 2026-08-19 fixes * test(server): ws/voice/controls — test-audit 2026-08-19 fixes * test(server): ws/voice/join — test-audit 2026-08-19 fixes * test(server): ws/voice/moderation — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/commands.rs — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/secret_store.rs — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/update_commands.rs — test-audit 2026-08-19 fixes * test(client): src/components/ChannelSidebar.ts — test-audit 2026-08-19 fixes * test(client): src/lib/ws.ts — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/credentials.rs — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/tofu.rs — test-audit 2026-08-19 fixes * test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 fixes * test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 fixes * test(client): src/pages/connect-page/LoginForm.ts — test-audit 2026-08-19 fixes * test(client): src/pages/main-page/SidebarArea.ts — test-audit 2026-08-19 fixes * test(client): src/stores/voice.store.ts — test-audit 2026-08-19 fixes * test(client): tests/browser/smoke.test.ts — test-audit 2026-08-19 fixes * test(client): tests/unit/media.test.ts — test-audit 2026-08-19 fixes * test(client): tests/unit/renderers.test.ts — test-audit 2026-08-19 fixes * test(client): src/components/UserProfilePopup.ts — test-audit 2026-08-19 fixes * test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 fixes * test(client): tests/unit/log-persistence.test.ts — test-audit 2026-08-19 fixes * test(client): keep tests/browser out of the jsdom suite and run it in CI * test(server): ws/hub_broadcast_test.go — bytes.Equal payload compare (gocritic) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(client): src/lib/credentials.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/dispatcher.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/permissions.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/stores/messages.store.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/ws.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/identity.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/livekitE2EE.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/stores/auth.store.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/stores/voice.store.ts — test-audit 2026-08-19 round 2 (Stryker) * docs: test audit 2026-08-19 — findings, fixes, measured baselines Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(graph): refresh the knowledge graph after the 2026-08-19 test audit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
385 lines
12 KiB
TypeScript
385 lines
12 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { createUserProfilePopup, type UserProfileData } from "@components/UserProfilePopup";
|
|
|
|
function makeUser(overrides?: Partial<UserProfileData>): UserProfileData {
|
|
return {
|
|
id: 42,
|
|
username: "testuser",
|
|
avatar: null,
|
|
role: "member",
|
|
status: "online",
|
|
about: "Hello there!",
|
|
joinDate: "2024-01-15",
|
|
isDeleted: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("UserProfilePopup", () => {
|
|
let container: HTMLDivElement;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
container.remove();
|
|
});
|
|
|
|
it("click trigger opens popup with correct content", () => {
|
|
const user = makeUser({ username: "alice", role: "admin" });
|
|
const popup = createUserProfilePopup({
|
|
user,
|
|
anchorX: 200,
|
|
anchorY: 300,
|
|
});
|
|
popup.mount(container);
|
|
|
|
const popupEl = container.querySelector('[data-testid="user-profile-popup"]');
|
|
expect(popupEl).not.toBeNull();
|
|
|
|
// Check username displayed
|
|
const nameEl = container.querySelector(".upp-username");
|
|
expect(nameEl?.textContent).toBe("alice");
|
|
|
|
// Check role badge
|
|
const roleBadge = container.querySelector(".upp-role-badge");
|
|
expect(roleBadge?.textContent).toContain("Admin");
|
|
|
|
// Check about section
|
|
const about = container.querySelector(".upp-about-text");
|
|
expect(about?.textContent).toBe("Hello there!");
|
|
|
|
// Check dialog role
|
|
expect(popupEl?.getAttribute("role")).toBe("dialog");
|
|
expect(popupEl?.getAttribute("aria-label")).toBe("User profile");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("displays content correctly including status and join date", () => {
|
|
const user = makeUser({
|
|
username: "bob",
|
|
status: "dnd",
|
|
joinDate: "2023-06-01",
|
|
});
|
|
const popup = createUserProfilePopup({
|
|
user,
|
|
anchorX: 100,
|
|
anchorY: 100,
|
|
onMessage: () => {},
|
|
onCall: () => {},
|
|
});
|
|
popup.mount(container);
|
|
|
|
// Status label should show "Do Not Disturb"
|
|
const statusLine = container.querySelector(".upp-status-line");
|
|
expect(statusLine?.textContent).toContain("Do Not Disturb");
|
|
|
|
// Join date
|
|
const joinText = container.querySelector(".upp-join-text");
|
|
expect(joinText?.textContent).toBe("2023-06-01");
|
|
|
|
// Avatar initial for "bob" should be "B"
|
|
const avatar = container.querySelector(".upp-avatar span");
|
|
expect(avatar?.textContent).toBe("B");
|
|
|
|
// Message and Call buttons render when handlers are wired
|
|
const msgBtn = container.querySelector('[data-testid="upp-message-btn"]');
|
|
expect(msgBtn).not.toBeNull();
|
|
const callBtn = container.querySelector('[data-testid="upp-call-btn"]');
|
|
expect(callBtn).not.toBeNull();
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("omits action buttons that have no handler", () => {
|
|
const popup = createUserProfilePopup({
|
|
user: makeUser(),
|
|
anchorX: 100,
|
|
anchorY: 100,
|
|
});
|
|
popup.mount(container);
|
|
|
|
expect(container.querySelector('[data-testid="upp-message-btn"]')).toBeNull();
|
|
expect(container.querySelector('[data-testid="upp-call-btn"]')).toBeNull();
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("outside click closes the popup", () => {
|
|
const user = makeUser();
|
|
const popup = createUserProfilePopup({
|
|
user,
|
|
anchorX: 200,
|
|
anchorY: 200,
|
|
});
|
|
popup.mount(container);
|
|
|
|
expect(popup.isOpen()).toBe(true);
|
|
|
|
// Simulate a mousedown on the overlay (outside the popup)
|
|
const overlay = container.querySelector('[data-testid="user-profile-overlay"]') as HTMLElement;
|
|
expect(overlay).not.toBeNull();
|
|
|
|
const event = new MouseEvent("mousedown", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: 1,
|
|
clientY: 1,
|
|
});
|
|
overlay.dispatchEvent(event);
|
|
|
|
expect(popup.isOpen()).toBe(false);
|
|
expect(container.querySelector('[data-testid="user-profile-popup"]')).toBeNull();
|
|
});
|
|
|
|
it("handles deleted users with [deleted] name and gray avatar", () => {
|
|
const user = makeUser({
|
|
username: "removed",
|
|
isDeleted: true,
|
|
});
|
|
const popup = createUserProfilePopup({
|
|
user,
|
|
anchorX: 100,
|
|
anchorY: 100,
|
|
});
|
|
popup.mount(container);
|
|
|
|
const nameEl = container.querySelector(".upp-username");
|
|
expect(nameEl?.textContent).toBe("[deleted]");
|
|
|
|
// Avatar should have gray background
|
|
const avatar = container.querySelector(".upp-avatar") as HTMLElement;
|
|
expect(avatar.style.background).toBe("rgb(78, 80, 88)"); // #4e5058
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("Escape key closes the popup", () => {
|
|
const user = makeUser();
|
|
const popup = createUserProfilePopup({
|
|
user,
|
|
anchorX: 200,
|
|
anchorY: 200,
|
|
});
|
|
popup.mount(container);
|
|
|
|
expect(popup.isOpen()).toBe(true);
|
|
|
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
|
|
|
expect(popup.isOpen()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("UserProfilePopup positioning", () => {
|
|
let container: HTMLDivElement;
|
|
let originalInnerWidth: number;
|
|
let originalInnerHeight: number;
|
|
let offsetHeightDescriptor: PropertyDescriptor | undefined;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
originalInnerWidth = window.innerWidth;
|
|
originalInnerHeight = window.innerHeight;
|
|
});
|
|
|
|
afterEach(() => {
|
|
container.remove();
|
|
window.innerWidth = originalInnerWidth;
|
|
window.innerHeight = originalInnerHeight;
|
|
if (offsetHeightDescriptor) {
|
|
Object.defineProperty(HTMLElement.prototype, "offsetHeight", offsetHeightDescriptor);
|
|
offsetHeightDescriptor = undefined;
|
|
}
|
|
});
|
|
|
|
function getPopupEl(): HTMLElement {
|
|
return container.querySelector('[data-testid="user-profile-popup"]') as HTMLElement;
|
|
}
|
|
|
|
it("places the card to the right of the anchor, offset by the gap, when it fits", () => {
|
|
window.innerWidth = 800;
|
|
window.innerHeight = 600;
|
|
const popup = createUserProfilePopup({ user: makeUser(), anchorX: 100, anchorY: 100 });
|
|
popup.mount(container);
|
|
|
|
// 100 + 8 (gap) = 108, and 108 + 300 (width) = 408 fits inside 800 - 8, so
|
|
// no flip. jsdom never lays anything out (offsetHeight is 0), so nothing
|
|
// overflows the bottom either and top stays at the anchor.
|
|
expect(getPopupEl().style.left).toBe("108px");
|
|
expect(getPopupEl().style.top).toBe("100px");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("flips the card to the left of the anchor when there is no room on the right", () => {
|
|
window.innerWidth = 1024;
|
|
window.innerHeight = 768;
|
|
const popup = createUserProfilePopup({ user: makeUser(), anchorX: 1020, anchorY: 100 });
|
|
popup.mount(container);
|
|
|
|
// 1020 + 8 + 300 = 1328 overflows the 1016px right bound, so it flips to
|
|
// sit left of the anchor instead: 1020 - 300 - 8 = 712.
|
|
expect(getPopupEl().style.left).toBe("712px");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("clamps the left edge to the viewport margin when even the flipped position runs off both edges", () => {
|
|
window.innerWidth = 200;
|
|
window.innerHeight = 600;
|
|
const popup = createUserProfilePopup({ user: makeUser(), anchorX: 50, anchorY: 100 });
|
|
popup.mount(container);
|
|
|
|
// The 300px-wide card can't fit on either side of a 200px-wide window, so
|
|
// the flip still overflows negative and gets clamped to the margin.
|
|
expect(getPopupEl().style.left).toBe("8px");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("lifts the card above the anchor so it fits when it would run off the bottom of the window", () => {
|
|
window.innerWidth = 1024;
|
|
window.innerHeight = 500;
|
|
// jsdom never lays anything out, so offsetHeight is always 0. Stub the
|
|
// popup card's measured height so the overflow branch actually triggers.
|
|
offsetHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
|
|
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
configurable: true,
|
|
get(this: HTMLElement) {
|
|
return this.dataset.testid === "user-profile-popup" ? 400 : 0;
|
|
},
|
|
});
|
|
const popup = createUserProfilePopup({ user: makeUser(), anchorX: 10, anchorY: 450 });
|
|
popup.mount(container);
|
|
|
|
// 450 + 400 (measured height) = 850 overflows the 492px bottom bound, so
|
|
// the card is lifted to 500 - 400 - 8 = 92.
|
|
expect(getPopupEl().style.top).toBe("92px");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("clamps the top edge to the viewport margin when the anchor is near the top edge", () => {
|
|
window.innerWidth = 1024;
|
|
window.innerHeight = 768;
|
|
const popup = createUserProfilePopup({ user: makeUser(), anchorX: 10, anchorY: 2 });
|
|
popup.mount(container);
|
|
|
|
expect(getPopupEl().style.top).toBe("8px");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
});
|
|
|
|
// ─── Phase 6: display name, about, custom status ─────────────────────────────
|
|
|
|
describe("UserProfilePopup profile fields", () => {
|
|
let container: HTMLDivElement;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
container.remove();
|
|
});
|
|
|
|
it("shows the display name as the heading and the username as an @handle", () => {
|
|
const popup = createUserProfilePopup({
|
|
user: makeUser({ username: "alice", displayName: "Alice A." }),
|
|
anchorX: 10,
|
|
anchorY: 10,
|
|
});
|
|
popup.mount(container);
|
|
|
|
expect(container.querySelector(".upp-username")?.textContent).toBe("Alice A.");
|
|
// The username is still the handle you @mention, so the popup keeps
|
|
// telling you what to type.
|
|
expect(container.querySelector(".upp-username-handle")?.textContent).toBe("@alice");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("omits the @handle when there is no display name", () => {
|
|
const popup = createUserProfilePopup({
|
|
user: makeUser({ username: "alice", displayName: null }),
|
|
anchorX: 10,
|
|
anchorY: 10,
|
|
});
|
|
popup.mount(container);
|
|
|
|
expect(container.querySelector(".upp-username")?.textContent).toBe("alice");
|
|
expect(container.querySelector(".upp-username-handle")?.textContent).toBe("");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("renders the about section from real data", () => {
|
|
const popup = createUserProfilePopup({
|
|
user: makeUser({ about: "Writes tests for a living." }),
|
|
anchorX: 10,
|
|
anchorY: 10,
|
|
});
|
|
popup.mount(container);
|
|
|
|
// The section used to be dead code — `about` was hardcoded null at every
|
|
// call site until phase 6 gave the column somewhere to come from.
|
|
expect(container.querySelector(".upp-about-text")?.textContent).toBe(
|
|
"Writes tests for a living.",
|
|
);
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("renders the custom status line, and nothing when there is none", () => {
|
|
const withStatus = createUserProfilePopup({
|
|
user: makeUser({ customStatus: "shipping phase 6" }),
|
|
anchorX: 10,
|
|
anchorY: 10,
|
|
});
|
|
withStatus.mount(container);
|
|
expect(container.querySelector(".upp-custom-status")?.textContent).toBe("shipping phase 6");
|
|
withStatus.destroy?.();
|
|
|
|
const without = createUserProfilePopup({ user: makeUser(), anchorX: 10, anchorY: 10 });
|
|
without.mount(container);
|
|
expect(container.querySelector(".upp-custom-status")?.textContent).toBe("");
|
|
without.destroy?.();
|
|
});
|
|
|
|
it("labels the owner's own invisible status", () => {
|
|
// Only ever reachable for the signed-in user: everyone else is mapped to
|
|
// offline before the payload leaves the server.
|
|
const popup = createUserProfilePopup({
|
|
user: makeUser({ status: "invisible" }),
|
|
anchorX: 10,
|
|
anchorY: 10,
|
|
});
|
|
popup.mount(container);
|
|
|
|
const statusText = container.querySelector(".upp-status-line")?.textContent ?? "";
|
|
expect(statusText).toContain("Invisible");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
|
|
it("uses the display name's initial for the letter fallback", () => {
|
|
const popup = createUserProfilePopup({
|
|
user: makeUser({ username: "alice", displayName: "Zoe", avatar: null }),
|
|
anchorX: 10,
|
|
anchorY: 10,
|
|
});
|
|
popup.mount(container);
|
|
|
|
expect(container.querySelector(".upp-avatar .avatar-initial")?.textContent).toBe("Z");
|
|
|
|
popup.destroy?.();
|
|
});
|
|
});
|