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");
|
||||
|
||||
Reference in New Issue
Block a user