From ff73a11eabb7283c9c5f6ec37aa7b91cd4f4615f Mon Sep 17 00:00:00 2001 From: ANIMASHAUN Michael Date: Wed, 17 Jun 2026 17:14:43 +0100 Subject: [PATCH 1/3] feat: tenant switcher shown when user enrolled on other tenants not main --- components/header/profile/tenant-select.tsx | 2 ++ components/header/profile/user-profile-button.tsx | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 From 6a90b10cb6a2033b0589f972de1f316e49bcca29 Mon Sep 17 00:00:00 2001 From: ANIMASHAUN Michael Date: Wed, 17 Jun 2026 17:19:51 +0100 Subject: [PATCH 2/3] feat: tenant switcher shown when user enrolled on other tenants not main > test coverage --- .../__tests__/user-profile-button.test.tsx | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) 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', () => { From cfcd87e2748b561c0d85b6103003e5a9b289f7dc Mon Sep 17 00:00:00 2001 From: ANIMASHAUN Michael Date: Wed, 17 Jun 2026 18:12:42 +0100 Subject: [PATCH 3/3] feat: tenant switcher shown when user enrolled on other tenants not main > test coverage --- .../profile/__tests__/tenant-select.test.tsx | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 components/header/profile/__tests__/tenant-select.test.tsx 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'); + }); + }); +});