refactor: organize frontend components into features/ directories

Moved 43 component files into 9 feature domains:
- features/git/ — commit-dialog, commit-panel, file-editor, git-mount-editor,
  git-toolbar, merge-dialog
- features/project/ — repositories-settings-tab, repository-create-dialog
- features/terminal/ — special-keys-panel, special-keys-strip,
  terminal-session-tabs, terminal
- features/workspace/ — workspace-card, workspace-create-form,
  workspace-header, workspace-instance-chips
- features/session/ — create-session-form, session-card, session-list
- features/tool/ — instance-list, manifest-editor, start-tool-fab,
  start-tool-modal, tool-starter, tools-bottom-sheet
- features/notification/ — event-toast-bridge, notification-center,
  notification-item
- features/settings/ — settings-tab-layout
- features/mobile/ — mobile-action-sheet, mobile-detail-view, mobile-edit-view,
  mobile-fab, mobile-list-view, mobile-nav, mobile-page-header,
  mobile-terminal-header, mobile-terminal-wrapper

Updated all imports across pages and components.
Root components/ now only contains generic UI pieces:
app-shell, code-editor, data-states, icon, protected-route, syntax-highlighter.

Quality gates: verified no remaining old imports.
This commit is contained in:
2026-06-04 12:37:24 +02:00
parent 7224afafd1
commit 1021d61be3
54 changed files with 31 additions and 31 deletions
@@ -0,0 +1,177 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { NotificationCenter } from "./notification-center";
import { NotificationProvider } from "../state/notifications";
vi.mock("../api/notifications", () => ({
getNotifications: vi.fn(),
getUnreadCount: vi.fn(),
markNotificationRead: vi.fn(),
markAllNotificationsRead: vi.fn(),
dismissNotification: vi.fn(),
clearAllNotifications: vi.fn(),
}));
import { getNotifications, getUnreadCount } from "../api/notifications";
const mockedGetNotifications = vi.mocked(getNotifications);
const mockedGetUnreadCount = vi.mocked(getUnreadCount);
const makeNotification = (id: string, overrides?: Record<string, unknown>) => ({
id,
user_id: "user-1",
category: "instance",
severity: "info" as const,
title: `Notification ${id}`,
message: null,
source_type: null,
source_id: null,
metadata: {},
read_at: null,
dismissed_at: null,
created_at: "2026-05-29T10:00:00Z",
...overrides,
});
function wrapper({ children }: { children: React.ReactNode }) {
return <NotificationProvider>{children}</NotificationProvider>;
}
describe("NotificationCenter", () => {
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
mockedGetNotifications.mockResolvedValue({
items: [],
total: 0,
limit: 20,
offset: 0,
});
mockedGetUnreadCount.mockResolvedValue(0);
});
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
cleanup();
});
it("renders bell icon", () => {
render(<NotificationCenter />, { wrapper });
expect(
screen.getByRole("button", { name: /notifications/i }),
).toBeInTheDocument();
});
it("shows badge when unread count > 0", async () => {
mockedGetUnreadCount.mockResolvedValue(3);
render(<NotificationCenter />, { wrapper });
await vi.advanceTimersByTimeAsync(100);
expect(screen.getByText("3")).toBeInTheDocument();
});
it("hides badge when unread count is 0", () => {
render(<NotificationCenter />, { wrapper });
expect(screen.queryByText("0")).not.toBeInTheDocument();
});
it("opens dropdown on bell click", () => {
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
it("closes dropdown on outside click", () => {
render(
<div>
<div data-testid="outside">Outside</div>
<NotificationCenter />
</div>,
{ wrapper },
);
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
fireEvent.mouseDown(screen.getByTestId("outside"));
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("closes dropdown on escape", () => {
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
fireEvent.keyDown(document, { key: "Escape" });
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("renders empty state when no notifications", () => {
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
expect(screen.getByText("No notifications")).toBeInTheDocument();
});
it("renders notification items", async () => {
mockedGetNotifications.mockResolvedValue({
items: [makeNotification("1"), makeNotification("2")],
total: 2,
limit: 20,
offset: 0,
});
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
await vi.advanceTimersByTimeAsync(100);
expect(screen.getByText("Notification 1")).toBeInTheDocument();
expect(screen.getByText("Notification 2")).toBeInTheDocument();
});
it("calls markAllRead on footer button click", async () => {
mockedGetNotifications.mockResolvedValue({
items: [makeNotification("1")],
total: 1,
limit: 20,
offset: 0,
});
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
await vi.advanceTimersByTimeAsync(100);
fireEvent.click(screen.getByRole("button", { name: /mark all as read/i }));
const { markAllNotificationsRead: mockMarkAll } = await import(
"../api/notifications"
);
expect(vi.mocked(mockMarkAll)).toHaveBeenCalled();
});
it("calls clearAll on clear-all button click", async () => {
mockedGetNotifications.mockResolvedValue({
items: [makeNotification("1")],
total: 1,
limit: 20,
offset: 0,
});
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
await vi.advanceTimersByTimeAsync(100);
fireEvent.click(screen.getByRole("button", { name: /clear all/i }));
const { clearAllNotifications: mockClearAll } = await import(
"../api/notifications"
);
expect(vi.mocked(mockClearAll)).toHaveBeenCalled();
});
it("refreshes list immediately on open", async () => {
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
await vi.advanceTimersByTimeAsync(100);
expect(mockedGetNotifications).toHaveBeenCalled();
});
});