Files
multica/packages/views/workspace/create-workspace-form.test.tsx
Bohan Jiang 6f29a4c0a6 fix(workspace): derive workspace URL prefix from deployment host (MUL-3400) (#4286)
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>
2026-06-18 11:12:10 +08:00

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();
});
});