diff --git a/calm-hub-ui/src/components/logout-button/LogoutButton.test.tsx b/calm-hub-ui/src/components/logout-button/LogoutButton.test.tsx
new file mode 100644
index 000000000..e226a1baa
--- /dev/null
+++ b/calm-hub-ui/src/components/logout-button/LogoutButton.test.tsx
@@ -0,0 +1,50 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { LogoutButton } from './LogoutButton.js';
+import { authService } from '../../authService.js';
+
+vi.mock('../../authService.js', () => ({
+ authService: {
+ logout: vi.fn(),
+ },
+}));
+
+describe('LogoutButton', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders a logout button', () => {
+ render();
+ const button = screen.getByRole('button', { name: /logout/i });
+ expect(button).toBeInTheDocument();
+ });
+
+ it('has correct styling', () => {
+ render();
+ const button = screen.getByRole('button', { name: /logout/i });
+ expect(button).toHaveStyle({
+ position: 'absolute',
+ top: '10px',
+ right: '10px',
+ });
+ });
+
+ it('calls authService.logout when clicked', async () => {
+ render();
+ const button = screen.getByRole('button', { name: /logout/i });
+
+ fireEvent.click(button);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ expect(authService.logout).toHaveBeenCalled();
+ });
+
+ it('button text is "Logout"', () => {
+ render();
+ const button = screen.getByRole('button');
+ expect(button.textContent).toBe('Logout');
+ });
+});
+
diff --git a/calm-hub-ui/src/components/logout-button/LogoutButton.tsx b/calm-hub-ui/src/components/logout-button/LogoutButton.tsx
new file mode 100644
index 000000000..bf932c495
--- /dev/null
+++ b/calm-hub-ui/src/components/logout-button/LogoutButton.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { authService } from '../../authService.js';
+
+export const LogoutButton: React.FC = () => {
+ const handleLogout = async () => {
+ await authService.logout();
+ };
+
+ return (
+
+ );
+};
diff --git a/calm-hub-ui/src/index.tsx b/calm-hub-ui/src/index.tsx
index d498b6c47..e574d9e85 100644
--- a/calm-hub-ui/src/index.tsx
+++ b/calm-hub-ui/src/index.tsx
@@ -2,23 +2,12 @@ import './index.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import ProtectedRoute from './ProtectedRoute.js';
-import { isAuthServiceEnabled, authService } from './authService.js';
+import { isAuthServiceEnabled } from './authService.js';
import App from './App.js';
+import { LogoutButton } from './components/logout-button/LogoutButton.js';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
-const LogoutButton: React.FC = () => {
- const handleLogout = async () => {
- await authService.logout();
- };
-
- return (
-
- );
-};
-
const isAuthenticationEnabled = isAuthServiceEnabled();
root.render(
diff --git a/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.test.tsx b/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.test.tsx
index 00ae95083..253d2c42a 100644
--- a/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.test.tsx
+++ b/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.test.tsx
@@ -1,7 +1,8 @@
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/react';
-import { EdgeBadge, getBadgeStyle } from './EdgeBadge';
-import { THEME } from '../theme';
+import { EdgeBadge } from './EdgeBadge.js';
+import { getBadgeStyle } from '../utils/edgeBadge.utils.js';
+import { THEME } from '../theme.js';
describe('EdgeBadge', () => {
const mockOnMouseEnter = vi.fn();
diff --git a/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.tsx b/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.tsx
index b0f866a91..50cbcb019 100644
--- a/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.tsx
+++ b/calm-hub-ui/src/visualizer/components/reactflow/edge-components/EdgeBadge.tsx
@@ -1,6 +1,5 @@
import { Info, Shield, ArrowRight } from 'lucide-react';
-import { THEME } from '../theme.js';
-import type { EdgeBadgeStyle, EdgeBadgeProps } from '../../../contracts/contracts.js';
+import type { EdgeBadgeProps } from '../../../contracts/contracts.js';
export function EdgeBadge({
hasFlowInfo,
@@ -36,25 +35,3 @@ export function EdgeBadge({
);
}
-
-export function getBadgeStyle(hasFlowInfo: boolean, hasAIGF: boolean): EdgeBadgeStyle {
- if (hasFlowInfo) {
- return {
- background: `${THEME.colors.accent}20`,
- border: THEME.colors.accent,
- iconColor: THEME.colors.accent,
- };
- }
- if (hasAIGF) {
- return {
- background: `${THEME.colors.success}20`,
- border: THEME.colors.success,
- iconColor: THEME.colors.success,
- };
- }
- return {
- background: `${THEME.colors.muted}20`,
- border: THEME.colors.muted,
- iconColor: THEME.colors.muted,
- };
-}
diff --git a/calm-hub-ui/src/visualizer/components/reactflow/edge-components/index.ts b/calm-hub-ui/src/visualizer/components/reactflow/edge-components/index.ts
index 429165199..29a5eeae1 100644
--- a/calm-hub-ui/src/visualizer/components/reactflow/edge-components/index.ts
+++ b/calm-hub-ui/src/visualizer/components/reactflow/edge-components/index.ts
@@ -1,4 +1,5 @@
-export { EdgeBadge, getBadgeStyle } from './EdgeBadge.js';
+export { EdgeBadge } from './EdgeBadge.js';
+export { getBadgeStyle } from '../utils/edgeBadge.utils.js';
export { EdgeTooltip } from './EdgeTooltip.js';
export type {
EdgeBadgeProps,
diff --git a/calm-hub-ui/src/visualizer/components/reactflow/utils/edgeBadge.utils.test.ts b/calm-hub-ui/src/visualizer/components/reactflow/utils/edgeBadge.utils.test.ts
new file mode 100644
index 000000000..a66e6a65e
--- /dev/null
+++ b/calm-hub-ui/src/visualizer/components/reactflow/utils/edgeBadge.utils.test.ts
@@ -0,0 +1,48 @@
+import { describe, it, expect } from 'vitest';
+import { getBadgeStyle } from './edgeBadge.utils.js';
+import { THEME } from '../theme.js';
+
+describe('edgeBadge.utils', () => {
+ describe('getBadgeStyle', () => {
+ it('returns accent colors when hasFlowInfo is true', () => {
+ const result = getBadgeStyle(true, false);
+ expect(result).toEqual({
+ background: `${THEME.colors.accent}20`,
+ border: THEME.colors.accent,
+ iconColor: THEME.colors.accent,
+ });
+ });
+
+ it('returns success colors when hasAIGF is true', () => {
+ const result = getBadgeStyle(false, true);
+ expect(result).toEqual({
+ background: `${THEME.colors.success}20`,
+ border: THEME.colors.success,
+ iconColor: THEME.colors.success,
+ });
+ });
+
+ it('returns muted colors when both flags are false', () => {
+ const result = getBadgeStyle(false, false);
+ expect(result).toEqual({
+ background: `${THEME.colors.muted}20`,
+ border: THEME.colors.muted,
+ iconColor: THEME.colors.muted,
+ });
+ });
+
+ it('prioritizes hasFlowInfo over hasAIGF when both are true', () => {
+ const result = getBadgeStyle(true, true);
+ expect(result).toEqual({
+ background: `${THEME.colors.accent}20`,
+ border: THEME.colors.accent,
+ iconColor: THEME.colors.accent,
+ });
+ });
+
+ it('has the correct alpha value for background colors', () => {
+ const result = getBadgeStyle(true, false);
+ expect(result.background).toMatch(/^#[0-9a-f]+20$/i);
+ });
+ });
+});
diff --git a/calm-hub-ui/src/visualizer/components/reactflow/utils/edgeBadge.utils.ts b/calm-hub-ui/src/visualizer/components/reactflow/utils/edgeBadge.utils.ts
new file mode 100644
index 000000000..f200b8e4c
--- /dev/null
+++ b/calm-hub-ui/src/visualizer/components/reactflow/utils/edgeBadge.utils.ts
@@ -0,0 +1,24 @@
+import { THEME } from '../theme.js';
+import type { EdgeBadgeStyle } from '../../../contracts/contracts.js';
+
+export function getBadgeStyle(hasFlowInfo: boolean, hasAIGF: boolean): EdgeBadgeStyle {
+ if (hasFlowInfo) {
+ return {
+ background: `${THEME.colors.accent}20`,
+ border: THEME.colors.accent,
+ iconColor: THEME.colors.accent,
+ };
+ }
+ if (hasAIGF) {
+ return {
+ background: `${THEME.colors.success}20`,
+ border: THEME.colors.success,
+ iconColor: THEME.colors.success,
+ };
+ }
+ return {
+ background: `${THEME.colors.muted}20`,
+ border: THEME.colors.muted,
+ iconColor: THEME.colors.muted,
+ };
+}