diff --git a/components/header/profile/__tests__/tenant-select.test.tsx b/components/header/profile/__tests__/tenant-select.test.tsx
new file mode 100644
index 0000000..0b6ec18
--- /dev/null
+++ b/components/header/profile/__tests__/tenant-select.test.tsx
@@ -0,0 +1,138 @@
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { TenantSelect } from '../tenant-select';
+
+// Hoisted mock functions
+const { mockHandleTenantSwitch } = vi.hoisted(() => ({
+ mockHandleTenantSwitch: vi.fn(),
+}));
+
+// Mutable state for per-test config control
+let mockTenantKey = 'test-tenant';
+let mockTenants: { key: string; org: string }[] = [
+ { key: 'test-tenant', org: 'test-org' },
+ { key: 'other-tenant', org: 'other-org' },
+];
+let mockEnableRBAC = false;
+
+// Mock helpers
+vi.mock('@/utils/helpers', () => ({
+ getTenant: () => mockTenantKey,
+ getTenants: () => mockTenants,
+ handleTenantSwitch: mockHandleTenantSwitch,
+}));
+
+// Mock web-utils (Tenant is a type-only export at runtime)
+vi.mock('@iblai/iblai-js/web-utils', () => ({
+ Tenant: {},
+}));
+
+// Mock config
+vi.mock('@/lib/config', () => ({
+ config: {
+ settings: {
+ enableRBAC: () => mockEnableRBAC,
+ },
+ },
+}));
+
+// Mock Redux hooks
+vi.mock('@/lib/hooks', () => ({
+ useAppSelector: (selector: any) => selector({ rbac: {} }),
+}));
+
+// Mock RBAC slice
+vi.mock('@/features/rbac', () => ({
+ selectRbacPermissions: () => ({ canSwitch: true }),
+}));
+
+// Mock TenantSwitcher
+vi.mock('@iblai/iblai-js/web-containers', () => ({
+ TenantSwitcher: (props: any) => (
+
+ {props.currentTenantKey}
+ {String(props.tenants?.length)}
+ {String(props.enableRbac)}
+ {JSON.stringify(props.rbacPermissions)}
+
+
+ ),
+}));
+
+describe('TenantSelect', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockTenantKey = 'test-tenant';
+ mockTenants = [
+ { key: 'test-tenant', org: 'test-org' },
+ { key: 'other-tenant', org: 'other-org' },
+ ];
+ mockEnableRBAC = false;
+ });
+
+ describe('rendering', () => {
+ it('renders the TenantSwitcher when a tenant key and tenants exist', () => {
+ render();
+
+ expect(screen.getByTestId('tenant-switcher')).toBeInTheDocument();
+ });
+
+ it('passes the current tenant key to the switcher', () => {
+ render();
+
+ expect(screen.getByTestId('current-tenant-key')).toHaveTextContent('test-tenant');
+ });
+
+ it('passes the tenants list to the switcher', () => {
+ render();
+
+ expect(screen.getByTestId('tenant-count')).toHaveTextContent('2');
+ });
+
+ it('passes the RBAC permissions to the switcher', () => {
+ render();
+
+ expect(screen.getByTestId('rbac-permissions')).toHaveTextContent(
+ JSON.stringify({ canSwitch: true }),
+ );
+ });
+
+ it('passes the enableRbac config flag to the switcher', () => {
+ mockEnableRBAC = true;
+ render();
+
+ expect(screen.getByTestId('enable-rbac')).toHaveTextContent('true');
+ });
+ });
+
+ describe('empty states', () => {
+ it('renders nothing when there are no tenants', () => {
+ mockTenants = [];
+ const { container } = render();
+
+ expect(screen.queryByTestId('tenant-switcher')).not.toBeInTheDocument();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('renders nothing when the tenant key is empty', () => {
+ mockTenantKey = '';
+ const { container } = render();
+
+ expect(screen.queryByTestId('tenant-switcher')).not.toBeInTheDocument();
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+
+ describe('callbacks', () => {
+ it('wires onTenantChange to handleTenantSwitch', () => {
+ render();
+
+ fireEvent.click(screen.getByTestId('change-btn'));
+
+ expect(mockHandleTenantSwitch).toHaveBeenCalledWith('new-tenant');
+ });
+ });
+});
diff --git a/components/header/profile/__tests__/user-profile-button.test.tsx b/components/header/profile/__tests__/user-profile-button.test.tsx
index 4bfa107..e56584c 100644
--- a/components/header/profile/__tests__/user-profile-button.test.tsx
+++ b/components/header/profile/__tests__/user-profile-button.test.tsx
@@ -37,16 +37,18 @@ vi.mock('@/utils/helpers', () => ({
// Mock local storage hooks - default to admin
let mockIsAdmin = true;
+const defaultUserTenants = [
+ { key: 'test-tenant', is_admin: true, org: 'test-org' },
+ { key: 'other-tenant', is_admin: false, org: 'other-org' },
+];
+let mockUserTenants: { key: string; is_admin: boolean; org: string }[] = defaultUserTenants;
vi.mock('@/utils/localstorage', () => ({
useCurrentTenant: () => ({
currentTenant: { key: 'test-tenant', is_admin: true, org: 'test-org' },
saveCurrentTenant: mockSaveCurrentTenant,
}),
useUserTenants: () => ({
- userTenants: [
- { key: 'test-tenant', is_admin: true, org: 'test-org' },
- { key: 'other-tenant', is_admin: false, org: 'other-org' },
- ],
+ userTenants: mockUserTenants,
saveUserTenants: mockSaveUserTenants,
}),
useIsAdmin: () => mockIsAdmin,
@@ -150,6 +152,7 @@ describe('UserProfileButton', () => {
mockEnableGravatarOnProfilePic = 'true'; // Reset to gravatar enabled by default
mockDefaultSupportPhoneNumber = '(571) 293-0242'; // Reset to config default
mockTenantMetadata = undefined; // Reset to no tenant metadata
+ mockUserTenants = defaultUserTenants; // Reset to default tenant list
});
describe('rendering', () => {
@@ -210,12 +213,27 @@ describe('UserProfileButton', () => {
expect(screen.getByTestId('show-tenant-switcher')).toHaveTextContent('true');
});
- it('should not show tenant switcher for non-admin users', () => {
+ it('should not show tenant switcher for non-admin users with no other tenants', () => {
mockIsAdmin = false;
+ // User is only enrolled on the current tenant (plus main), so there are no
+ // other non-main tenants to switch to.
+ mockUserTenants = [
+ { key: 'test-tenant', is_admin: false, org: 'test-org' },
+ { key: 'main', is_admin: false, org: 'main-org' },
+ ];
render();
expect(screen.getByTestId('show-tenant-switcher')).toHaveTextContent('false');
});
+
+ it('should show tenant switcher for non-admin users enrolled on other tenants', () => {
+ mockIsAdmin = false;
+ // Default tenant list includes 'other-tenant', a non-main tenant other than
+ // the current one, so the switcher should be shown even for non-admins.
+ render();
+
+ expect(screen.getByTestId('show-tenant-switcher')).toHaveTextContent('true');
+ });
});
describe('callbacks', () => {
diff --git a/components/header/profile/tenant-select.tsx b/components/header/profile/tenant-select.tsx
index 940cd3d..74d22d3 100644
--- a/components/header/profile/tenant-select.tsx
+++ b/components/header/profile/tenant-select.tsx
@@ -4,6 +4,7 @@ import { Tenant } from '@iblai/iblai-js/web-utils';
import _ from 'lodash';
import { selectRbacPermissions } from '@/features/rbac';
import { useAppSelector } from '@/lib/hooks';
+import { config } from '@/lib/config';
export function TenantSelect() {
const tenantKey = getTenant();
@@ -20,6 +21,7 @@ export function TenantSelect() {
tenants={tenants}
onTenantChange={handleTenantSwitch}
rbacPermissions={rbacPermissions}
+ enableRbac={config.settings.enableRBAC()}
/>
);
}
diff --git a/components/header/profile/user-profile-button.tsx b/components/header/profile/user-profile-button.tsx
index ca745ee..eee8b9d 100644
--- a/components/header/profile/user-profile-button.tsx
+++ b/components/header/profile/user-profile-button.tsx
@@ -66,7 +66,9 @@ export const UserProfileButton = () => {
// Configuration
showProfileTab={true}
showAccountTab={false} // Skills app doesn't have account tab
- showTenantSwitcher={isAdmin}
+ showTenantSwitcher={
+ isAdmin || userTenants.some((t) => t.key !== 'main' && t.key !== tenantKey)
+ }
showHelpLink={false} // Skills app doesn't have help link in dropdown
showLogoutButton={true}
showLearnerModeSwitch={false} // Skills app doesn't have learner mode switch