Skip to content
Merged
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
7 changes: 6 additions & 1 deletion frontend/src/app/campus/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { Metadata } from 'next';
import { MetaverseCampus } from '../../components/Metaverse';
import ErrorBoundary from '../../components/ErrorBoundary';

export const metadata: Metadata = {
title: 'Metaverse Campus — StarkEd',
description: 'Immersive virtual learning campus with classrooms, social spaces, and avatar interaction.',
};

export default function CampusPage() {
return <MetaverseCampus />;
return (
<ErrorBoundary>
<MetaverseCampus />
</ErrorBoundary>
);
}
7 changes: 6 additions & 1 deletion frontend/src/app/lab/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { Metadata } from 'next';
import { VirtualScienceLab } from '../../components/Lab';
import ErrorBoundary from '../../components/ErrorBoundary';

export const metadata: Metadata = {
title: 'Virtual Science Laboratory — StarkEd',
description: 'Interactive virtual lab for experiments with 3D equipment, guided steps, safety warnings, and collaboration.'
};

export default function LabPage() {
return <VirtualScienceLab />;
return (
<ErrorBoundary>
<VirtualScienceLab />
</ErrorBoundary>
);
}
5 changes: 4 additions & 1 deletion frontend/src/components/BCI/BCIDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HandsFreeNavigation } from './HandsFreeNavigation';
import { AttentionTracker } from './AttentionTracker';
import { AdaptiveDifficulty } from './AdaptiveDifficulty';
import { NeurofeedbackTraining } from './NeurofeedbackTraining';
import { RouteErrorBoundary } from '../RouteErrorBoundary';

type TabType = 'dashboard' | 'navigation' | 'attention' | 'difficulty' | 'training';

Expand Down Expand Up @@ -168,7 +169,9 @@ export const BCIDashboard: React.FC = () => {
</div>

<div className="space-y-6">
{renderActiveTab()}
<RouteErrorBoundary routeName={tabs.find(tab => tab.id === activeTab)?.name || 'BCI Tab'}>
{renderActiveTab()}
</RouteErrorBoundary>
</div>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ interface State {
}

export class ErrorBoundary extends Component<Props, State> {
public static defaultProps = {
onReset: undefined
};
public state: State = {
hasError: false
};
Expand Down Expand Up @@ -120,3 +123,5 @@ export class ErrorBoundary extends Component<Props, State> {
return this.props.children;
}
}

export default ErrorBoundary;
3 changes: 3 additions & 0 deletions frontend/src/components/NanoLearning/NanoLearningHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useNeuralInterface } from '../../hooks/useNeuralInterface';
import { useSkillAcquisition } from '../../hooks/useSkillAcquisition';
import { useNanotechMonitoring } from '../../hooks/useNanotechMonitoring';
import type { Skill } from '../../types/nanotech';
import ErrorBoundary from '../ErrorBoundary';

interface NanoLearningHubProps {
userId: string;
Expand Down Expand Up @@ -90,6 +91,7 @@ export function NanoLearningHub({
const isSafetyCompromised = safety.safetyStatus && safety.safetyStatus.status !== 'safe';

return (
<ErrorBoundary>
<div className="w-full max-w-6xl mx-auto p-6 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 rounded-2xl border border-cyan-500/20 shadow-2xl">
{/* Header */}
<div className="mb-8">
Expand Down Expand Up @@ -300,5 +302,6 @@ export function NanoLearningHub({
</div>
)}
</div>
</ErrorBoundary>
);
}
5 changes: 5 additions & 0 deletions frontend/src/components/RouteErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ interface State {
}

export class RouteErrorBoundary extends Component<Props, State> {
public static defaultProps = {
routeName: undefined
};
public state: State = {
hasError: false
};
Expand Down Expand Up @@ -106,3 +109,5 @@ export class RouteErrorBoundary extends Component<Props, State> {
return this.props.children;
}
}

export default RouteErrorBoundary;
128 changes: 128 additions & 0 deletions frontend/src/components/__tests__/BCIDashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { BCIDashboard } from '../BCI/BCIDashboard';

jest.mock('../BCI/CognitiveDashboard', () => ({
CognitiveDashboard: () => { throw new Error('Cognitive dashboard crash'); }
}));

jest.mock('../BCI/HandsFreeNavigation', () => ({
HandsFreeNavigation: () => <div>HandsFree Navigation Content</div>
}));

jest.mock('../BCI/AttentionTracker', () => ({
AttentionTracker: () => <div>Attention Tracker Content</div>
}));

jest.mock('../BCI/AdaptiveDifficulty', () => ({
AdaptiveDifficulty: () => <div>Adaptive Difficulty Content</div>
}));

jest.mock('../BCI/NeurofeedbackTraining', () => ({
NeurofeedbackTraining: () => <div>Neurofeedback Training Content</div>
}));

function suppressConsoleError() {
(console.error as any).mockRestore?.();
return jest.spyOn(console, 'error').mockImplementation(() => {});
}

function restoreConsoleError(spy: jest.SpyInstance) {
spy.mockRestore();
}

function setNodeEnv(value: string) {
Object.assign(process.env, { NODE_ENV: value });
}

const ORIGINAL_ENV = { ...process.env };

describe('BCIDashboard error boundary', () => {
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
Object.assign(process.env, { NODE_ENV: ORIGINAL_ENV.NODE_ENV });
consoleErrorSpy = suppressConsoleError();
});

afterEach(() => {
restoreConsoleError(consoleErrorSpy);
});

describe('when active tab crashes', () => {
it('shows RouteErrorBoundary fallback for the crashing tab', () => {
render(<BCIDashboard />);
expect(screen.getByText(/could not load cognitive monitor/i)).toBeInTheDocument();
expect(screen.getByText('Retry Section')).toBeInTheDocument();
});

it('sidebar navigation remains functional when a tab crashes', () => {
render(<BCIDashboard />);
expect(screen.getByText(/could not load cognitive monitor/i)).toBeInTheDocument();

fireEvent.click(screen.getByText('Hands-Free Control'));
expect(screen.getByText('HandsFree Navigation Content')).toBeInTheDocument();
});

it('switching to a non-crashing tab recovers from error', () => {
render(<BCIDashboard />);
expect(screen.getByText(/could not load cognitive monitor/i)).toBeInTheDocument();

fireEvent.click(screen.getByText('Attention Tracking'));
expect(screen.getByText('Attention Tracker Content')).toBeInTheDocument();
});
});

describe('error details in dev mode', () => {
afterEach(() => {
setNodeEnv(ORIGINAL_ENV.NODE_ENV || 'test');
});

it('shows error message in development mode', () => {
setNodeEnv('development');
render(<BCIDashboard />);
expect(screen.getByText('Cognitive dashboard crash')).toBeInTheDocument();
});

it('shows "Error Details" collapsible section in dev mode', () => {
setNodeEnv('development');
render(<BCIDashboard />);
expect(screen.getByText('Error Details')).toBeInTheDocument();
});
});

describe('error handling in production mode', () => {
afterEach(() => {
setNodeEnv(ORIGINAL_ENV.NODE_ENV || 'test');
});

it('shows generic message in production mode', () => {
setNodeEnv('production');
render(<BCIDashboard />);
expect(screen.getByText(/an error occurred loading this section/i)).toBeInTheDocument();
expect(screen.queryByText('Cognitive dashboard crash')).not.toBeInTheDocument();
});

it('does not show "Error Details" section in production mode', () => {
setNodeEnv('production');
render(<BCIDashboard />);
expect(screen.queryByText('Error Details')).not.toBeInTheDocument();
});
});

describe('accessibility', () => {
it('error UI has proper heading structure', () => {
render(<BCIDashboard />);
const headings = screen.getAllByRole('heading', { level: 3 });
const errorHeading = headings.find(h => h.textContent?.includes('Could not load'));
expect(errorHeading).toBeTruthy();
expect(errorHeading).toHaveTextContent(/could not load cognitive monitor/i);
});

it('has an accessible retry button', () => {
render(<BCIDashboard />);
expect(screen.getByRole('button', { name: /retry section/i })).toBeInTheDocument();
});
});
});
92 changes: 92 additions & 0 deletions frontend/src/components/__tests__/NanoLearningHub.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { NanoLearningHub } from '../NanoLearning/NanoLearningHub';
import type { Skill } from '../../types/nanotech';

jest.mock('../../hooks/useNeuralInterface', () => ({
useNeuralInterface: jest.fn(() => ({
neuralPattern: null,
error: null,
startMonitoring: jest.fn(),
stopMonitoring: jest.fn(),
}))
}));

jest.mock('../../hooks/useSkillAcquisition', () => ({
useSkillAcquisition: jest.fn(() => ({
swarmStatus: null,
error: null,
initiateTransfer: jest.fn(),
stopTransfer: jest.fn(),
}))
}));

jest.mock('../../hooks/useNanotechMonitoring', () => ({
useNanotechMonitoring: jest.fn(() => ({
safetyStatus: null,
error: null,
startMonitoring: jest.fn(),
stopMonitoring: jest.fn(),
emergencyShutdown: jest.fn(),
}))
}));

function suppressConsoleError() {
(console.error as any).mockRestore?.();
return jest.spyOn(console, 'error').mockImplementation(() => {});
}

function restoreConsoleError(spy: jest.SpyInstance) {
spy.mockRestore();
}

const mockSkills: Skill[] = [
{
id: 'skill-1',
name: 'Neural Programming',
difficulty: 3,
category: 'technical',
} as Skill,
];

describe('NanoLearningHub error boundary', () => {
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
consoleErrorSpy = suppressConsoleError();
});

afterEach(() => {
restoreConsoleError(consoleErrorSpy);
});

describe('when no error occurs', () => {
it('renders the hub content normally', () => {
render(<NanoLearningHub userId="test-user" availableSkills={mockSkills} />);
expect(screen.getByText(/nanotechnology learning hub/i)).toBeInTheDocument();
expect(screen.getByText('Neural Programming')).toBeInTheDocument();
});

it('does not show ErrorBoundary fallback UI', () => {
render(<NanoLearningHub userId="test-user" availableSkills={mockSkills} />);
expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
expect(screen.queryByText('Try Again')).not.toBeInTheDocument();
expect(screen.queryByText('Go Home')).not.toBeInTheDocument();
});
});

describe('with error boundary wrapping', () => {
it('renders without crashing with empty skills', () => {
render(<NanoLearningHub userId="test-user" availableSkills={[]} />);
expect(screen.getByText(/no skills available/i)).toBeInTheDocument();
});

it('shows the main UI sections', () => {
render(<NanoLearningHub userId="test-user" availableSkills={mockSkills} />);
expect(screen.getByText(/available skills/i)).toBeInTheDocument();
const neuralHeadings = screen.getAllByText(/neural monitoring/i);
expect(neuralHeadings.length).toBeGreaterThan(0);
});
});
});
Loading