mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 22:17:48 +02:00
The create-workspace and onboarding UI hardcoded `multica.ai/` as the workspace slug URL prefix, so self-hosted deployments showed the wrong domain. Add a `workspaceUrlHost` helper that derives the host from the deployment's app URL (`daemon_app_url` from `/api/config`, via the config store) and falls back to the brand host when none is configured, then use it in both views. Fixes #4263. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { I18nProvider } from "@multica/core/i18n/react";
|
|
import { configStore } from "@multica/core/config";
|
|
import enCommon from "../locales/en/common.json";
|
|
import enWorkspace from "../locales/en/workspace.json";
|
|
import { CreateWorkspaceForm } from "./create-workspace-form";
|
|
|
|
const TEST_RESOURCES = {
|
|
en: { common: enCommon, workspace: enWorkspace },
|
|
};
|
|
|
|
const mockMutate = vi.fn();
|
|
vi.mock("@multica/core/workspace/mutations", () => ({
|
|
useCreateWorkspace: () => ({ mutate: mockMutate, isPending: false }),
|
|
}));
|
|
|
|
function I18nWrapper({ children }: { children: ReactNode }) {
|
|
return (
|
|
<I18nProvider locale="en" resources={TEST_RESOURCES}>
|
|
{children}
|
|
</I18nProvider>
|
|
);
|
|
}
|
|
|
|
function renderForm(onSuccess = vi.fn()) {
|
|
const qc = new QueryClient();
|
|
return render(
|
|
<QueryClientProvider client={qc}>
|
|
<CreateWorkspaceForm onSuccess={onSuccess} />
|
|
</QueryClientProvider>,
|
|
{ wrapper: I18nWrapper },
|
|
);
|
|
}
|
|
|
|
describe("CreateWorkspaceForm", () => {
|
|
beforeEach(() => {
|
|
mockMutate.mockReset();
|
|
configStore.setState({ daemonAppUrl: "" });
|
|
});
|
|
|
|
it("shows the brand host as the URL prefix when no app URL is configured", () => {
|
|
renderForm();
|
|
expect(screen.getByText("multica.ai/")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the deployment host as the URL prefix for self-hosted instances", () => {
|
|
configStore.setState({ daemonAppUrl: "https://multica.example.com" });
|
|
renderForm();
|
|
expect(screen.getByText("multica.example.com/")).toBeInTheDocument();
|
|
expect(screen.queryByText("multica.ai/")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("auto-generates slug from name until user edits slug", () => {
|
|
renderForm();
|
|
fireEvent.change(screen.getByLabelText(/workspace name/i), {
|
|
target: { value: "Acme Corp" },
|
|
});
|
|
expect(screen.getByDisplayValue("acme-corp")).toBeInTheDocument();
|
|
});
|
|
|
|
it("stops auto-generating slug once user edits slug directly", () => {
|
|
renderForm();
|
|
fireEvent.change(screen.getByLabelText(/workspace url/i), {
|
|
target: { value: "custom" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText(/workspace name/i), {
|
|
target: { value: "Different Name" },
|
|
});
|
|
expect(screen.getByDisplayValue("custom")).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls onSuccess with the created workspace", async () => {
|
|
const onSuccess = vi.fn();
|
|
mockMutate.mockImplementation((_args, opts) => {
|
|
opts?.onSuccess?.({ id: "ws-1", slug: "acme", name: "Acme" });
|
|
});
|
|
renderForm(onSuccess);
|
|
fireEvent.change(screen.getByLabelText(/workspace name/i), {
|
|
target: { value: "Acme" },
|
|
});
|
|
fireEvent.click(screen.getByRole("button", { name: /create workspace/i }));
|
|
await waitFor(() =>
|
|
expect(onSuccess).toHaveBeenCalledWith(
|
|
expect.objectContaining({ slug: "acme" }),
|
|
),
|
|
);
|
|
});
|
|
|
|
it("shows slug-conflict error inline on 409", async () => {
|
|
mockMutate.mockImplementation((_args, opts) => {
|
|
opts?.onError?.({ status: 409 });
|
|
});
|
|
renderForm();
|
|
fireEvent.change(screen.getByLabelText(/workspace name/i), {
|
|
target: { value: "Taken" },
|
|
});
|
|
fireEvent.click(screen.getByRole("button", { name: /create workspace/i }));
|
|
await waitFor(() =>
|
|
expect(screen.getByText(/already taken/i)).toBeInTheDocument(),
|
|
);
|
|
});
|
|
|
|
it("disables submit when slug has invalid format", () => {
|
|
renderForm();
|
|
fireEvent.change(screen.getByLabelText(/workspace name/i), {
|
|
target: { value: "Valid Name" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText(/workspace url/i), {
|
|
target: { value: "Invalid Slug!" },
|
|
});
|
|
expect(
|
|
screen.getByRole("button", { name: /create workspace/i }),
|
|
).toBeDisabled();
|
|
});
|
|
|
|
it("disables submit when slug is reserved", () => {
|
|
renderForm();
|
|
fireEvent.change(screen.getByLabelText(/workspace name/i), {
|
|
target: { value: "Valid Name" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText(/workspace url/i), {
|
|
target: { value: "admin" },
|
|
});
|
|
expect(
|
|
screen.getByRole("button", { name: /create workspace/i }),
|
|
).toBeDisabled();
|
|
expect(screen.getByText(/reserved and cannot be used/i)).toBeInTheDocument();
|
|
});
|
|
});
|