Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ src-tauri/gen/apple/
.cache/
.yalc
.yalc.lock
yalc.lock

# signing artifacts (never commit provisioning profiles)
*.provisionprofile
Expand Down
96 changes: 96 additions & 0 deletions app/onboarding/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';

const { state, push } = vi.hoisted(() => ({
state: { tenantKey: 'acme' },
push: vi.fn(),
}));

vi.mock('@/hooks/use-onboarding', () => ({
useOnboardingAccess: () => ({ canAccess: true, tenantKey: state.tenantKey }),
}));

vi.mock('@/hooks/use-user', () => ({ useUsername: () => 'tester' }));
vi.mock('next/navigation', () => ({ useRouter: () => ({ push }) }));

vi.mock('@/lib/initial-loader', () => ({ hideInitialLoader: vi.fn() }));
vi.mock('@/components/spinner', () => ({ Spinner: () => <div>spinner</div> }));

// The OS-local final screen; stub it so the page test doesn't pull the data layer.
vi.mock('@/components/onboarding/onboarding-create-agent-step', () => ({
OnboardingCreateAgentStep: () => <div>create-agent-step</div>,
}));

// The route renders the SDK wizard from @iblai/iblai-js/web-containers; stub it,
// expose onComplete, and render whatever final step the page passes. The wizard
// persists the answers itself (default), so the page no longer wires that.
vi.mock('@iblai/iblai-js/web-containers', () => ({
OnboardingWizard: ({
tenant,
username,
onComplete,
renderFinalStep,
}: {
tenant: string;
username: string;
onComplete: (agentId: string | null) => void;
renderFinalStep?: (context: unknown) => React.ReactNode;
}) => (
<div>
<div>
onboarding-wizard:{tenant}:{username}
</div>
<button type="button" onClick={() => onComplete('agent-1')}>
complete-with-agent
</button>
<button type="button" onClick={() => onComplete(null)}>
complete-no-agent
</button>
<div>
{renderFinalStep?.({
tenant,
username,
answers: { organizationName: '', sector: null },
goBack: () => {},
complete: () => {},
})}
</div>
</div>
),
}));

import OnboardingPage from '../page';

describe('OnboardingPage', () => {
beforeEach(() => {
vi.clearAllMocks();
state.tenantKey = 'acme';
});

it('renders the SDK wizard with the tenant + username and the OS create-agent final step', () => {
render(<OnboardingPage />);
expect(
screen.getByText('onboarding-wizard:acme:tester'),
).toBeInTheDocument();
expect(screen.getByText('create-agent-step')).toBeInTheDocument();
});

it('shows a loader while the current tenant resolves', () => {
state.tenantKey = '';
render(<OnboardingPage />);
expect(screen.getByText('spinner')).toBeInTheDocument();
expect(screen.queryByText(/onboarding-wizard/)).not.toBeInTheDocument();
});

it('redirects to the agent explore page on complete (tenant-level when no agent)', () => {
render(<OnboardingPage />);
fireEvent.click(
screen.getByRole('button', { name: 'complete-with-agent' }),
);
expect(push).toHaveBeenCalledWith('/platform/acme/agent-1/explore');

fireEvent.click(screen.getByRole('button', { name: 'complete-no-agent' }));
expect(push).toHaveBeenCalledWith('/platform/acme/explore');
});
});
53 changes: 53 additions & 0 deletions app/onboarding/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { OnboardingWizard } from '@iblai/iblai-js/web-containers';

import { Spinner } from '@/components/spinner';
import { hideInitialLoader } from '@/lib/initial-loader';
import { useOnboardingAccess } from '@/hooks/use-onboarding';
import { useUsername } from '@/hooks/use-user';
import { OnboardingCreateAgentStep } from '@/components/onboarding/onboarding-create-agent-step';

/**
* The `/onboarding` route renders the SDK onboarding wizard
* (`@iblai/iblai-js/web-containers`). The app supplies identity (tenant +
* username) and the create-agent final step; the wizard persists the answers
* itself (under the user's platform metadata `onboarding` key) since no
* `onAnswersSubmit` is passed. On completion it redirects to an agent's explore
* page. It performs NO automatic redirects into onboarding; access gating is
* wired up elsewhere. While the current tenant resolves, a brief loader shows.
*/
export default function OnboardingPage() {
const { tenantKey } = useOnboardingAccess();
const username = useUsername();
const router = useRouter();

useEffect(() => {
hideInitialLoader();
}, []);

if (!tenantKey) {
return (
<div className="flex h-dvh w-screen items-center justify-center">
<Spinner className="h-14 w-14" />
</div>
);
}

return (
<OnboardingWizard
tenant={tenantKey}
username={username ?? ''}
onComplete={(agentId) =>
router.push(
agentId
? `/platform/${tenantKey}/${agentId}/explore`
: `/platform/${tenantKey}/explore`,
)
}
renderFinalStep={(context) => <OnboardingCreateAgentStep {...context} />}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

const { mockCreateMentor, mockUnwrap } = vi.hoisted(() => ({
mockCreateMentor: vi.fn(),
mockUnwrap: vi.fn(),
}));
const mockUseCreateMentorMutation = vi.fn();
const mockUseGetMentorsQuery = vi.fn();

vi.mock('@iblai/iblai-js/data-layer', () => ({
useCreateMentorMutation: (...args: unknown[]) =>
mockUseCreateMentorMutation(...args),
useGetMentorsQuery: (...args: unknown[]) => mockUseGetMentorsQuery(...args),
}));

// Stub the SDK shell pieces so the test doesn't load the whole web-containers bundle.
vi.mock('@iblai/iblai-js/web-containers', () => ({
StepHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
onboardingPrimaryButtonClass: 'btn',
}));

vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock('@/lib/config', () => ({
config: { iblTemplateMentor: () => 'ai-mentor' },
}));

import { OnboardingCreateAgentStep } from '../onboarding-create-agent-step';

const baseContext = {
tenant: 'acme',
username: 'tester',
answers: { organizationName: 'Acme', sector: 'higher_education' },
suggestedAgent: { name: 'Campus Assistant', description: 'Helps students.' },
goBack: vi.fn(),
};

beforeEach(() => {
vi.clearAllMocks();
mockUnwrap.mockResolvedValue({ unique_id: 'new-agent' });
mockCreateMentor.mockReturnValue({ unwrap: mockUnwrap });
mockUseCreateMentorMutation.mockReturnValue([
mockCreateMentor,
{ isLoading: false },
]);
mockUseGetMentorsQuery.mockReturnValue({
data: { count: 1, results: [{ unique_id: 'existing-agent' }] },
isLoading: false,
});
});

describe('OnboardingCreateAgentStep', () => {
it('prefills name + description from the sector suggestion', () => {
render(<OnboardingCreateAgentStep {...baseContext} complete={vi.fn()} />);
expect(screen.getByText('Make your first agent')).toBeInTheDocument();
expect((screen.getByLabelText('Name') as HTMLInputElement).value).toBe(
'Campus Assistant',
);
expect(
(screen.getByLabelText('Description') as HTMLTextAreaElement).value,
).toBe('Helps students.');
});

it('kicks off creation without waiting and completes with an existing agent id', async () => {
// Slow endpoint — never resolves, to prove we don't wait for it.
mockUnwrap.mockReturnValue(new Promise(() => {}));
const complete = vi.fn();
render(<OnboardingCreateAgentStep {...baseContext} complete={complete} />);

fireEvent.click(screen.getByRole('button', { name: 'Create agent' }));

await waitFor(() => expect(mockCreateMentor).toHaveBeenCalledTimes(1));
const payload = mockCreateMentor.mock.calls[0][0];
expect(payload.org).toBe('acme');
expect(payload.userId).toBe('tester');
expect(payload.formData.new_mentor_name).toBe('Campus Assistant');
expect(payload.formData.mentor_visibility).toBe(
'viewable_by_tenant_students',
);
expect(payload.formData.metadata).toEqual({ category: null });
expect(payload.formData.categories).toBeUndefined();

// Completes IMMEDIATELY with the EXISTING agent's id (not the new one).
expect(complete).toHaveBeenCalledWith('existing-agent');
});

it('completes with null when the tenant has no agents yet', () => {
mockUseGetMentorsQuery.mockReturnValue({
data: { count: 0, results: [] },
isLoading: false,
});
const complete = vi.fn();
render(<OnboardingCreateAgentStep {...baseContext} complete={complete} />);
fireEvent.click(screen.getByRole('button', { name: 'Create agent' }));
expect(complete).toHaveBeenCalledWith(null);
});
});
Loading
Loading