Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions components/header/profile/__tests__/tenant-select.test.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div data-testid="tenant-switcher">
<span data-testid="current-tenant-key">{props.currentTenantKey}</span>
<span data-testid="tenant-count">{String(props.tenants?.length)}</span>
<span data-testid="enable-rbac">{String(props.enableRbac)}</span>
<span data-testid="rbac-permissions">{JSON.stringify(props.rbacPermissions)}</span>
<button data-testid="change-btn" onClick={() => props.onTenantChange?.('new-tenant')}>
Change
</button>
</div>
),
}));

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(<TenantSelect />);

expect(screen.getByTestId('tenant-switcher')).toBeInTheDocument();
});

it('passes the current tenant key to the switcher', () => {
render(<TenantSelect />);

expect(screen.getByTestId('current-tenant-key')).toHaveTextContent('test-tenant');
});

it('passes the tenants list to the switcher', () => {
render(<TenantSelect />);

expect(screen.getByTestId('tenant-count')).toHaveTextContent('2');
});

it('passes the RBAC permissions to the switcher', () => {
render(<TenantSelect />);

expect(screen.getByTestId('rbac-permissions')).toHaveTextContent(
JSON.stringify({ canSwitch: true }),
);
});

it('passes the enableRbac config flag to the switcher', () => {
mockEnableRBAC = true;
render(<TenantSelect />);

expect(screen.getByTestId('enable-rbac')).toHaveTextContent('true');
});
});

describe('empty states', () => {
it('renders nothing when there are no tenants', () => {
mockTenants = [];
const { container } = render(<TenantSelect />);

expect(screen.queryByTestId('tenant-switcher')).not.toBeInTheDocument();
expect(container).toBeEmptyDOMElement();
});

it('renders nothing when the tenant key is empty', () => {
mockTenantKey = '';
const { container } = render(<TenantSelect />);

expect(screen.queryByTestId('tenant-switcher')).not.toBeInTheDocument();
expect(container).toBeEmptyDOMElement();
});
});

describe('callbacks', () => {
it('wires onTenantChange to handleTenantSwitch', () => {
render(<TenantSelect />);

fireEvent.click(screen.getByTestId('change-btn'));

expect(mockHandleTenantSwitch).toHaveBeenCalledWith('new-tenant');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(<UserProfileButton />);

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(<UserProfileButton />);

expect(screen.getByTestId('show-tenant-switcher')).toHaveTextContent('true');
});
});

describe('callbacks', () => {
Expand Down
2 changes: 2 additions & 0 deletions components/header/profile/tenant-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -20,6 +21,7 @@ export function TenantSelect() {
tenants={tenants}
onTenantChange={handleTenantSwitch}
rbacPermissions={rbacPermissions}
enableRbac={config.settings.enableRBAC()}
/>
);
}
4 changes: 3 additions & 1 deletion components/header/profile/user-profile-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading