Files
multica/packages/views/workspace/paths-hooks.test.tsx
Matt Voska 700cd97407 feat(workspace): add per-workspace logo upload (#2760)
Adds avatar_url column to workspace, threads it through the API +
WorkspaceAvatar component, and adds a click-to-upload editor in the
workspace settings tab. Mirrors the squad avatar pattern (migration 086);
UI strings use "logo" while the schema/code uses avatar_url for codebase
consistency with user.avatar_url and squad.avatar_url.

- migration 093: ALTER TABLE workspace ADD COLUMN avatar_url TEXT
- UpdateWorkspace SQL + handler accept avatar_url (auth gated to
  owner/admin at the router via RequireWorkspaceRoleFromURL)
- WorkspaceAvatar renders <img> when avatar_url is set, falls back to
  the initial-letter span otherwise
- workspace-tab.tsx adds a 16x16 click-to-upload logo editor at the
  top of the general settings card, using useFileUpload + accept=
  image/png,image/jpeg,image/webp (server stores under workspaces/{id}/)
- en + zh-Hans settings i18n strings added

Co-authored-by: Matt Voska <voska@users.noreply.github.com>
2026-06-01 16:48:05 +02:00

94 lines
3.0 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
WorkspaceSlugProvider,
useWorkspaceSlug,
useCurrentWorkspace,
} from "@multica/core/paths";
import { workspaceKeys } from "@multica/core/workspace/queries";
import type { Workspace } from "@multica/core/types";
// Hook tests for @multica/core/paths live here because packages/core/ runs
// Vitest in node environment (no jsdom). packages/views/ already has jsdom +
// @testing-library/react configured, so it's the correct home per CLAUDE.md
// testing rules ("shared UI components live in packages/views/*.test.tsx").
function makeWorkspace(over: Partial<Workspace>): Workspace {
return {
id: "id-default",
name: "Default",
slug: "default",
description: null,
context: null,
settings: {},
repos: [],
issue_prefix: "DEF",
avatar_url: null,
created_at: "",
updated_at: "",
...over,
};
}
function setup(slug: string | null, wsList: Workspace[] = []) {
const qc = new QueryClient();
qc.setQueryData(workspaceKeys.list(), wsList);
return function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={qc}>
<WorkspaceSlugProvider slug={slug}>{children}</WorkspaceSlugProvider>
</QueryClientProvider>
);
};
}
describe("useWorkspaceSlug", () => {
it("returns the provided slug", () => {
function Probe() {
return <div data-testid="slug">{useWorkspaceSlug() ?? "null"}</div>;
}
render(<Probe />, { wrapper: setup("acme") });
expect(screen.getByTestId("slug").textContent).toBe("acme");
});
it("returns null when no slug is provided", () => {
function Probe() {
return <div data-testid="slug">{useWorkspaceSlug() ?? "null"}</div>;
}
render(<Probe />, { wrapper: setup(null) });
expect(screen.getByTestId("slug").textContent).toBe("null");
});
});
describe("useCurrentWorkspace", () => {
const acme = makeWorkspace({ id: "id-1", slug: "acme", name: "Acme" });
it("resolves workspace from slug and list", () => {
function Probe() {
const ws = useCurrentWorkspace();
return <div data-testid="name">{ws?.name ?? "none"}</div>;
}
render(<Probe />, { wrapper: setup("acme", [acme]) });
expect(screen.getByTestId("name").textContent).toBe("Acme");
});
it("returns null when slug does not match any workspace", () => {
function Probe() {
const ws = useCurrentWorkspace();
return <div data-testid="name">{ws?.name ?? "none"}</div>;
}
render(<Probe />, { wrapper: setup("bogus", [acme]) });
expect(screen.getByTestId("name").textContent).toBe("none");
});
it("returns null when no slug is provided", () => {
function Probe() {
const ws = useCurrentWorkspace();
return <div data-testid="name">{ws?.name ?? "none"}</div>;
}
render(<Probe />, { wrapper: setup(null, [acme]) });
expect(screen.getByTestId("name").textContent).toBe("none");
});
});