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
50 changes: 50 additions & 0 deletions src/components/gamification/__tests__/LevelBadge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { LevelBadge } from '../LevelBadge/LevelBadge';
import type { LevelInfo } from '../../../types/gamification';

describe('LevelBadge', () => {
const mockLevel: LevelInfo = {
level: 5,
name: 'Pythonista Junior',
minXP: 1000,
maxXP: 2000,
icon: '🐍',
color: '#ff0000'
};

it('renders level info correctly', () => {
render(<LevelBadge level={mockLevel} currentXP={1500} />);

expect(screen.getByText('Nível 5')).toBeInTheDocument();
expect(screen.getByText('Pythonista Junior')).toBeInTheDocument();
expect(screen.getByText('🐍')).toBeInTheDocument();
});

it('shows progress bar when requested', () => {
render(<LevelBadge level={mockLevel} currentXP={1500} showProgress={true} />);

// 1500 XP out of 1000-2000 range means 500/1000 = 50%
expect(screen.getByText('500/1000 XP')).toBeInTheDocument();
});

it('handles max level (infinite XP)', () => {
const maxLevel = { ...mockLevel, maxXP: Infinity };
render(<LevelBadge level={maxLevel} currentXP={5000} showProgress={true} />);

expect(screen.getByText('MAX')).toBeInTheDocument();
});

it('calls onClick handler', () => {
const handleClick = vi.fn();
render(<LevelBadge level={mockLevel} currentXP={1500} onClick={handleClick} />);

fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalled();
});

it('applies size class', () => {
const { container } = render(<LevelBadge level={mockLevel} currentXP={1500} size="small" />);
expect(container.firstChild).toHaveClass('level-badge--small');
});
});
128 changes: 128 additions & 0 deletions src/components/gamification/__tests__/MissionList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MissionList } from '../MissionList/MissionList';
import type { Mission, UserMission } from '../../../types/gamification';

describe('MissionList', () => {
const mockDailyMissions: Mission[] = [
{
id: 'daily_1',
type: 'daily',
title: 'Daily Mission 1',
description: 'Description 1',
icon: '📅',
objectiveType: 'complete_questions',
targetValue: 5,
starsReward: 10,
xpReward: 50
}
];

const mockWeeklyMissions: Mission[] = [
{
id: 'weekly_1',
type: 'weekly',
title: 'Weekly Mission 1',
description: 'Description 2',
icon: '📆',
objectiveType: 'earn_stars',
targetValue: 20,
starsReward: 50,
xpReward: 200
}
];

const mockUserMissions: UserMission[] = [
{
missionId: 'daily_1',
progress: 2,
status: 'active',
expiresAt: new Date()
},
{
missionId: 'weekly_1',
progress: 20,
status: 'completed',
completedAt: new Date(),
expiresAt: new Date()
},
{
missionId: 'endgame_speedrun_0',
progress: 1,
status: 'active',
expiresAt: new Date()
}
];

it('renders daily and weekly missions', () => {
render(
<MissionList
dailyMissions={mockDailyMissions}
weeklyMissions={mockWeeklyMissions}
userMissions={mockUserMissions}
/>
);

expect(screen.getByText('Daily Mission 1')).toBeInTheDocument();
expect(screen.getByText('Weekly Mission 1')).toBeInTheDocument();
expect(screen.getByText('Missões Diárias')).toBeInTheDocument();
expect(screen.getByText('Missões Semanais')).toBeInTheDocument();
});

it('displays progress correctly', () => {
render(
<MissionList
dailyMissions={mockDailyMissions}
weeklyMissions={mockWeeklyMissions}
userMissions={mockUserMissions}
/>
);

expect(screen.getByText('2/5')).toBeInTheDocument(); // Daily progress
expect(screen.getByText('20/20')).toBeInTheDocument(); // Weekly progress
});

it('shows completed status', () => {
render(
<MissionList
dailyMissions={mockDailyMissions}
weeklyMissions={mockWeeklyMissions}
userMissions={mockUserMissions}
/>
);

const completedBadge = screen.getByText('✅ Completada!');
expect(completedBadge).toBeInTheDocument();
});

it('renders endgame missions if present', () => {
render(
<MissionList
dailyMissions={[]}
weeklyMissions={[]}
userMissions={mockUserMissions}
/>
);

// Based on logic, endgame_speedrun_0 should match an endgame mission definition
// or fallback to "Desafio Secreto" (but filtered out if logic fails, let's see)
// Actually, logic filters out "Desafio Secreto" titles.
// Assuming ENDGAME_MISSIONS in data has 'speedrun' type.
// If not, it won't render.
// Let's check if the section title renders
expect(screen.getByText('Desafios de Mestre')).toBeInTheDocument();
});

it('renders empty lists gracefully', () => {
render(
<MissionList
dailyMissions={[]}
weeklyMissions={[]}
userMissions={[]}
/>
);

expect(screen.getByText('Missões Diárias')).toBeInTheDocument();
expect(screen.queryByText('Daily Mission 1')).not.toBeInTheDocument();
});
});
200 changes: 200 additions & 0 deletions src/components/gamification/__tests__/PowerUpBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { PowerUpBar, PowerUpBarCompact } from '../PowerUpBar/PowerUpBar';
import type { UserPowerUps } from '../../../types/gamification';

describe('PowerUpBar', () => {
const mockUserPowerUps: UserPowerUps = {
inventory: {
skip: 1,
fifty_fifty: 0,
extra_hint: 2,
double_stars: 0,
shield: 0
},
usesToday: {
skip: 0,
fifty_fifty: 0,
extra_hint: 1,
double_stars: 0,
shield: 0
},
lastResetDate: new Date().toISOString().split('T')[0]
};

const mockHandlers = {
onUsePowerUp: vi.fn(),
onBuyPowerUp: vi.fn(),
};

it('renders all power-ups', () => {
render(
<PowerUpBar
userPowerUps={mockUserPowerUps}
userStars={100}
onUsePowerUp={mockHandlers.onUsePowerUp}
onBuyPowerUp={mockHandlers.onBuyPowerUp}
/>
);

// Check for specific powerup names (assuming default names in POWERUPS data)
expect(screen.getByText('Pular Questão')).toBeInTheDocument();
expect(screen.getByText('50/50')).toBeInTheDocument();
});

it('displays quantity and buttons correctly', () => {
render(
<PowerUpBar
userPowerUps={mockUserPowerUps}
userStars={100}
onUsePowerUp={mockHandlers.onUsePowerUp}
onBuyPowerUp={mockHandlers.onBuyPowerUp}
/>
);

// Skip has 1, should show "1x" and "Usar"
const skipButton = screen.getByRole('button', { name: /Usar Pular Questão/i });
expect(skipButton).toBeEnabled();
expect(screen.getAllByText('1x')).toHaveLength(1);

// Fifty-fifty has 0, should show price button
// Assuming price is 50 for 50/50 (check actual data or generic check)
// Since we don't know exact price from here without importing data, we look for buy button behavior
const buyButtons = screen.getAllByRole('button', { name: /Comprar/i });
expect(buyButtons.length).toBeGreaterThan(0);
});

it('calls onUsePowerUp when clicking use', () => {
render(
<PowerUpBar
userPowerUps={mockUserPowerUps}
userStars={100}
onUsePowerUp={mockHandlers.onUsePowerUp}
onBuyPowerUp={mockHandlers.onBuyPowerUp}
/>
);

const useBtn = screen.getByRole('button', { name: /Usar Pular Questão/i });
fireEvent.click(useBtn);
expect(mockHandlers.onUsePowerUp).toHaveBeenCalledWith('skip');
});

it('calls onBuyPowerUp when clicking buy', () => {
render(
<PowerUpBar
userPowerUps={mockUserPowerUps}
userStars={100}
onUsePowerUp={mockHandlers.onUsePowerUp}
onBuyPowerUp={mockHandlers.onBuyPowerUp}
/>
);

// Find a buy button (e.g., for 50/50)
const buyBtn = screen.getByRole('button', { name: /Comprar 50\/50/i });
fireEvent.click(buyBtn);
expect(mockHandlers.onBuyPowerUp).toHaveBeenCalled();
});

it('disables use button if max uses reached', () => {
const exhaustedPowerUps = {
...mockUserPowerUps,
usesToday: { ...mockUserPowerUps.usesToday, skip: 999 } // Assuming max is < 999
};

render(
<PowerUpBar
userPowerUps={exhaustedPowerUps}
userStars={100}
onUsePowerUp={mockHandlers.onUsePowerUp}
onBuyPowerUp={mockHandlers.onBuyPowerUp}
/>
);

const useBtn = screen.getByRole('button', { name: /Usar Pular Questão/i });
expect(useBtn).toBeDisabled();
});

it('disables buy button if not enough stars', () => {
render(
<PowerUpBar
userPowerUps={mockUserPowerUps}
userStars={0} // Poor user
onUsePowerUp={mockHandlers.onUsePowerUp}
onBuyPowerUp={mockHandlers.onBuyPowerUp}
/>
);

const buyButtons = screen.getAllByRole('button', { name: /Comprar/i });
buyButtons.forEach(btn => {
expect(btn).toBeDisabled();
});
});
});

describe('PowerUpBarCompact', () => {
const mockUserPowerUps: UserPowerUps = {
inventory: {
skip: 1,
fifty_fifty: 0,
extra_hint: 0,
double_stars: 0,
shield: 0
},
usesToday: {
skip: 0,
fifty_fifty: 0,
extra_hint: 0,
double_stars: 0,
shield: 0
},
lastResetDate: new Date().toISOString().split('T')[0]
};

const onUse = vi.fn();

it('renders only available power-ups', () => {
render(
<PowerUpBarCompact
userPowerUps={mockUserPowerUps}
onUsePowerUp={onUse}
/>
);

const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(1); // Only 'skip'
});

it('handles click', () => {
render(
<PowerUpBarCompact
userPowerUps={mockUserPowerUps}
onUsePowerUp={onUse}
/>
);

fireEvent.click(screen.getByRole('button'));
expect(onUse).toHaveBeenCalledWith('skip');
});

it('renders nothing if no powerups available', () => {
// Create a userPowerUps object where all inventory items are 0
const emptyUserPowerUps: UserPowerUps = {
...mockUserPowerUps,
inventory: {
skip: 0,
fifty_fifty: 0,
extra_hint: 0,
double_stars: 0,
shield: 0
}
};

render(
<PowerUpBarCompact
userPowerUps={emptyUserPowerUps}
onUsePowerUp={onUse}
/>
);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
Loading
Loading