feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars
- Artifacts hub: upload a file from the computer; make a skill by uploading a Markdown file, files, a folder or a .zip/.skill, or by writing it in the dialog; add plugins by picking from the list a marketplace link answers (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub. - Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid). - Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB, <data dir>/avatars/), shown with name and role at the sidebar's foot and on the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at). - Users page: per-user storage (GET /api/users/storage, measured in a worker) and account-pool spend/tokens per account (server/lib/pool-usage.js). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
* @file AccountSettings.test.tsx
|
||||
* @description Settings → Account: shows the account's own information, saves
|
||||
* only a changed display name/email, refuses a password change whose two new
|
||||
* passwords differ, sends the current + new password, and asks before signing
|
||||
* the other devices out.
|
||||
* passwords differ, sends the current + new password, asks before signing
|
||||
* the other devices out, and uploads/removes the avatar.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,9 @@ const me = vi.hoisted(() => ({
|
||||
updateAccount: vi.fn(),
|
||||
changePassword: vi.fn(),
|
||||
signOutOthers: vi.fn(),
|
||||
setAvatar: vi.fn(),
|
||||
removeAvatar: vi.fn(),
|
||||
avatarUrl: (at?: string | null) => (at ? `/api/me/avatar?v=${at}` : null),
|
||||
}));
|
||||
vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
@@ -44,7 +47,7 @@ const account = {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
for (const fn of Object.values(me)) fn.mockReset();
|
||||
for (const fn of Object.values(me)) if ("mockReset" in fn) fn.mockReset();
|
||||
me.account.mockResolvedValue(account);
|
||||
me.updateAccount.mockResolvedValue(account);
|
||||
me.changePassword.mockResolvedValue({ ok: true });
|
||||
@@ -53,6 +56,26 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("AccountSettings", () => {
|
||||
it("uploads a picked avatar and removes it again", async () => {
|
||||
const ue = userEvent.setup();
|
||||
const withAvatar = { ...account, user: { ...account.user, avatarAt: "2026-10-14T00:00:00Z" } };
|
||||
me.setAvatar.mockResolvedValue(withAvatar);
|
||||
me.removeAvatar.mockResolvedValue(account);
|
||||
render(<AccountSettings />);
|
||||
await screen.findByTestId("account-card");
|
||||
expect(screen.getByTestId("user-avatar-initials").textContent).toBe("Mi");
|
||||
expect(screen.queryByTestId("account-avatar-remove")).toBeNull();
|
||||
const file = new File([new Uint8Array([0x89, 0x50])], "me.png", { type: "image/png" });
|
||||
await ue.upload(screen.getByTestId("account-avatar-input"), file);
|
||||
await waitFor(() => expect(me.setAvatar).toHaveBeenCalledWith(file));
|
||||
expect((await screen.findByTestId("user-avatar-img")).getAttribute("src")).toBe(
|
||||
"/api/me/avatar?v=2026-10-14T00:00:00Z"
|
||||
);
|
||||
await ue.click(screen.getByTestId("account-avatar-remove"));
|
||||
await waitFor(() => expect(me.removeAvatar).toHaveBeenCalled());
|
||||
expect(await screen.findByTestId("user-avatar-initials")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the account's own information", async () => {
|
||||
render(<AccountSettings />);
|
||||
const card = await screen.findByTestId("account-card");
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @file Tests for ArtifactsHubSettings — the admin's Hub install switch in
|
||||
* Settings → AI tools: hidden in local mode and for a non-manager, and a
|
||||
* toggle saves through PUT /api/artifacts/settings.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import "../../../i18n";
|
||||
import { ArtifactsHubSettings } from "../ArtifactsHubSettings";
|
||||
import { api } from "../../../lib/api";
|
||||
|
||||
vi.mock("../../../lib/api", () => ({
|
||||
api: { artifacts: { settings: vi.fn(), setInstallsEnabled: vi.fn() } },
|
||||
}));
|
||||
|
||||
const settings = vi.mocked(api.artifacts.settings);
|
||||
const setInstalls = vi.mocked(api.artifacts.setInstallsEnabled);
|
||||
const base = { serverMode: true, canShare: true, installsEnabled: true, canManage: true };
|
||||
|
||||
describe("ArtifactsHubSettings", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("lets an admin turn Hub installs off", async () => {
|
||||
settings.mockResolvedValue(base);
|
||||
setInstalls.mockResolvedValue({ installsEnabled: false });
|
||||
render(<ArtifactsHubSettings />);
|
||||
const sw = await screen.findByRole("switch", { name: "Allow installing from the Hub" });
|
||||
fireEvent.click(sw);
|
||||
await waitFor(() => expect(setInstalls).toHaveBeenCalledWith(false));
|
||||
await waitFor(() => expect(sw).toHaveAttribute("aria-checked", "false"));
|
||||
});
|
||||
|
||||
it("renders nothing for a non-manager or in local mode", async () => {
|
||||
settings.mockResolvedValue({ ...base, canManage: false });
|
||||
const { container, unmount } = render(<ArtifactsHubSettings />);
|
||||
await waitFor(() => expect(settings).toHaveBeenCalled());
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
unmount();
|
||||
settings.mockResolvedValue({ ...base, serverMode: false });
|
||||
const r = render(<ArtifactsHubSettings />);
|
||||
await waitFor(() => expect(settings).toHaveBeenCalledTimes(2));
|
||||
expect(r.container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user