نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ApiError, api, apiRequest, getStoredToken, setStoredToken } from "@/lib/api";
|
||||
|
||||
describe("token storage", () => {
|
||||
it("round-trips a token through localStorage", () => {
|
||||
setStoredToken("abc123");
|
||||
expect(getStoredToken()).toBe("abc123");
|
||||
setStoredToken(null);
|
||||
expect(getStoredToken()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ApiError", () => {
|
||||
it("carries status, code and message", () => {
|
||||
const error = new ApiError(404, "not_found", "سازمان یافت نشد.");
|
||||
expect(error.status).toBe(404);
|
||||
expect(error.code).toBe("not_found");
|
||||
expect(error.message).toBe("سازمان یافت نشد.");
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it("carries the server's request id so a bug report is actionable", () => {
|
||||
const error = new ApiError(500, "internal_error", "خطای داخلی", undefined, "req-42");
|
||||
expect(error.requestId).toBe("req-42");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error correlation", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("reads the request id out of the error envelope", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: { code: "internal_error", message: "خطا", request_id: "req-from-body" },
|
||||
}),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(apiRequest("/api/whatever")).rejects.toMatchObject({
|
||||
requestId: "req-from-body",
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the X-Request-ID header", async () => {
|
||||
// A failure raised outside the error handlers — a CORS rejection or a proxy
|
||||
// error — still carries the header, and the user should still get an id.
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () =>
|
||||
new Response(JSON.stringify({ detail: "Disallowed CORS origin" }), {
|
||||
status: 400,
|
||||
headers: { "X-Request-ID": "req-from-header" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(apiRequest("/api/whatever")).rejects.toMatchObject({
|
||||
requestId: "req-from-header",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("error console client", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("omits unset filters from the query string", async () => {
|
||||
const calls: string[] = [];
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
calls.push(url);
|
||||
return new Response(JSON.stringify({ items: [], total: 0, limit: 50, offset: 0 }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await api.adminErrors({ status: "open", severity: "critical" });
|
||||
expect(calls[0]).toContain("status=open");
|
||||
expect(calls[0]).toContain("severity=critical");
|
||||
// An empty string means "all", and must not be sent as a filter value.
|
||||
expect(calls[0]).not.toContain("source=");
|
||||
expect(calls[0]).not.toContain("search=");
|
||||
});
|
||||
|
||||
it("sends no query string at all when there are no filters", async () => {
|
||||
const calls: string[] = [];
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
calls.push(url);
|
||||
return new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await api.adminErrorGroups();
|
||||
expect(calls[0]).not.toContain("?");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import {
|
||||
ArtifactViewer,
|
||||
Drawer,
|
||||
Modal,
|
||||
Table,
|
||||
Tabs,
|
||||
ToastStack,
|
||||
type TableColumn,
|
||||
} from "@agy/ui";
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<Row>> = [
|
||||
{ key: "name", header: "نام", render: (row) => row.name },
|
||||
];
|
||||
|
||||
describe("Table", () => {
|
||||
it("renders a row per record", () => {
|
||||
render(
|
||||
<Table
|
||||
columns={columns}
|
||||
rows={[{ id: "1", name: "کارمند الف" }, { id: "2", name: "کارمند ب" }]}
|
||||
rowKey={(row) => row.id}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("columnheader", { name: "نام" })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("row")).toHaveLength(3); // header + 2 body rows
|
||||
});
|
||||
|
||||
it("shows the empty state instead of an empty table body", () => {
|
||||
render(<Table columns={columns} rows={[]} rowKey={(row) => row.id} />);
|
||||
expect(screen.getByText("موردی برای نمایش نیست.")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("prefers the loading state over rows", () => {
|
||||
render(
|
||||
<Table
|
||||
columns={columns}
|
||||
rows={[{ id: "1", name: "کارمند الف" }]}
|
||||
rowKey={(row) => row.id}
|
||||
loading
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("status")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("prefers the error state over rows", () => {
|
||||
render(
|
||||
<Table
|
||||
columns={columns}
|
||||
rows={[{ id: "1", name: "کارمند الف" }]}
|
||||
rowKey={(row) => row.id}
|
||||
error="خطای شبکه"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tabs", () => {
|
||||
it("marks the active tab as selected and reports clicks", () => {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<Tabs
|
||||
items={[
|
||||
{ key: "overview", label: "نمای کلی" },
|
||||
{ key: "git", label: "گیت" },
|
||||
]}
|
||||
active="overview"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("tab", { name: "نمای کلی" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("tab", { name: "گیت" }));
|
||||
expect(onChange).toHaveBeenCalledWith("git");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Modal", () => {
|
||||
it("renders nothing while closed", () => {
|
||||
render(<Modal open={false} title="عنوان" onClose={() => undefined} />);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes when the close button is pressed", () => {
|
||||
const onClose = vi.fn();
|
||||
render(<Modal open title="عنوان" onClose={onClose} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "بستن" }));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Drawer", () => {
|
||||
it("renders its title and children when open", () => {
|
||||
render(
|
||||
<Drawer open title="جزئیات" onClose={() => undefined}>
|
||||
<p>محتوای کشو</p>
|
||||
</Drawer>,
|
||||
);
|
||||
expect(screen.getByRole("dialog", { name: "جزئیات" })).toBeInTheDocument();
|
||||
expect(screen.getByText("محتوای کشو")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToastStack", () => {
|
||||
it("renders nothing when there are no toasts", () => {
|
||||
const { container } = render(<ToastStack toasts={[]} onDismiss={() => undefined} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("dismisses a toast by id", () => {
|
||||
const onDismiss = vi.fn();
|
||||
render(
|
||||
<ToastStack
|
||||
toasts={[{ id: "t1", title: "ذخیره شد", tone: "success" }]}
|
||||
onDismiss={onDismiss}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("ذخیره شد")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "بستن" }));
|
||||
expect(onDismiss).toHaveBeenCalledWith("t1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ArtifactViewer", () => {
|
||||
it("shows the artifact content and kind", () => {
|
||||
render(<ArtifactViewer name="خروجی کار" kind="text" content="محتوای سند" />);
|
||||
expect(screen.getByText("خروجی کار")).toBeInTheDocument();
|
||||
expect(screen.getByText("text")).toBeInTheDocument();
|
||||
expect(screen.getByText("محتوای سند")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to a placeholder for an empty artifact", () => {
|
||||
render(<ArtifactViewer name="خالی" kind="text" content="" />);
|
||||
expect(screen.getByText("محتوایی ثبت نشده است.")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Badge, Button, EmptyState, ErrorState, LoadingState } from "@agy/ui";
|
||||
|
||||
describe("design system", () => {
|
||||
it("renders a button with its label", () => {
|
||||
render(<Button>ورود</Button>);
|
||||
expect(screen.getByRole("button", { name: "ورود" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables the button while loading", () => {
|
||||
render(<Button loading>ذخیره</Button>);
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("announces the loading state to assistive tech", () => {
|
||||
render(<LoadingState label="در حال بارگذاری…" />);
|
||||
expect(screen.getByRole("status")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an error state as an alert with a retry action", () => {
|
||||
render(<ErrorState message="خطای شبکه" onRetry={() => undefined} />);
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "تلاش دوباره" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an empty state title and description", () => {
|
||||
render(<EmptyState title="چیزی نیست" description="توضیح" />);
|
||||
expect(screen.getByText("چیزی نیست")).toBeInTheDocument();
|
||||
expect(screen.getByText("توضیح")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a badge", () => {
|
||||
render(<Badge tone="success">فعال</Badge>);
|
||||
expect(screen.getByText("فعال")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user