diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts index 9cfde54..986771a 100644 --- a/e2e/app.spec.ts +++ b/e2e/app.spec.ts @@ -22,7 +22,6 @@ test.describe('Task Manager App', () => { }); test('should display header stats', async ({ page }) => { - await expect(page.locator('#totalPoints')).toHaveText('0'); await expect(page.locator('#userLevel')).toHaveText('1'); await expect(page.locator('#dailyStreak')).toHaveText('0'); }); @@ -236,31 +235,6 @@ test.describe('Task Manager App', () => { }); }); - // ======================== - // Rewards Shop - // ======================== - test.describe('rewards shop', () => { - test.beforeEach(async ({ page }) => { - await page.click('[data-tab="shop"]'); - }); - - test('should show points display', async ({ page }) => { - await expect(page.locator('#shopPointsDisplay')).toHaveText('0'); - }); - - test('should create a reward', async ({ page }) => { - await page.click('#addRewardBtn'); - await page.fill('#rewardName', 'Movie Night'); - await page.fill('#rewardDescription', 'Watch a movie'); - await page.fill('#rewardCost', '100'); - await page.click('#rewardForm button[type="submit"]'); - - await expect(page.locator('#rewardModal')).not.toHaveClass(/active/); - await expect(page.locator('.project-card')).toBeVisible(); - await expect(page.locator('.project-title')).toContainText('Movie Night'); - }); - }); - // ======================== // Date Navigation // ======================== @@ -319,7 +293,6 @@ test.describe('Task Manager App', () => { title: 'Overdue Task', dueDate: dueDate, priority: 'medium', - points: 10, repeatType: 'none', completed: false, createdDate: new Date().toISOString() @@ -346,7 +319,6 @@ test.describe('Task Manager App', () => { title: 'Completed Old Task', dueDate: dueDate, priority: 'medium', - points: 10, repeatType: 'none', completed: true, completedDate: dueDate, @@ -383,9 +355,9 @@ test.describe('Task Manager App', () => { await page.evaluate(() => { const data = JSON.parse(localStorage.getItem('taskManagerData') || '{}'); data.tasks = [ - { id: 'task-high', title: 'High Task', priority: 'high', points: 10, repeatType: 'none', completed: false, createdDate: new Date().toISOString(), category: 'Work' }, - { id: 'task-medium', title: 'Medium Task', priority: 'medium', points: 10, repeatType: 'none', completed: false, createdDate: new Date().toISOString(), category: 'Personal' }, - { id: 'task-low', title: 'Low Task', priority: 'low', points: 10, repeatType: 'none', completed: false, createdDate: new Date().toISOString() }, + { id: 'task-high', title: 'High Task', priority: 'high', repeatType: 'none', completed: false, createdDate: new Date().toISOString(), category: 'Work' }, + { id: 'task-medium', title: 'Medium Task', priority: 'medium', repeatType: 'none', completed: false, createdDate: new Date().toISOString(), category: 'Personal' }, + { id: 'task-low', title: 'Low Task', priority: 'low', repeatType: 'none', completed: false, createdDate: new Date().toISOString() }, ]; localStorage.setItem('taskManagerData', JSON.stringify(data)); }); diff --git a/index.html b/index.html index 5d6c76e..95eab40 100644 --- a/index.html +++ b/index.html @@ -26,10 +26,6 @@

📋 Task Manager

-
- Points - 0 -
Level 1 @@ -64,7 +60,6 @@

📋 Task Manager

- @@ -203,10 +198,6 @@

Task Details

-
- - -
@@ -403,10 +394,6 @@

Daily Habit

-
- - -
@@ -568,62 +555,6 @@

Add Finance Item

- -
-
-
-

Reward Shop

- -
- -
-

Available Points

-

0

-
- - -
-

No rewards yet. Add rewards to spend your points on!

-
-
- - - -
-
diff --git a/src/app.ts b/src/app.ts index de6e6b2..d78c6c7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -23,7 +23,6 @@ class TaskManager { currentEditingHabitId: string | null = null; currentEditingFinanceId: string | null = null; currentEditingFinanceType: string | null = null; - currentEditingRewardId: string | null = null; currentEditingWishItemId: string | null = null; currentEditingNoteId: string | null = null; dragSrcWishId: string | null = null; @@ -132,12 +131,6 @@ class TaskManager { document.getElementById('prevMonthBtn')!.addEventListener('click', () => this.navigateToPrevMonth()); document.getElementById('nextMonthBtn')!.addEventListener('click', () => this.navigateToNextMonth()); - // Shop section - document.getElementById('addRewardBtn')!.addEventListener('click', () => this.openRewardModal()); - document.getElementById('rewardForm')!.addEventListener('submit', (e) => this.saveReward(e)); - document.getElementById('cancelRewardBtn')!.addEventListener('click', () => this.closeRewardModal()); - document.getElementById('deleteRewardBtn')!.addEventListener('click', () => this.deleteReward()); - // Wish List section document.getElementById('addWishItemBtn')!.addEventListener('click', () => this.openWishItemModal()); document.getElementById('wishItemForm')!.addEventListener('submit', (e) => this.saveWishItem(e)); @@ -262,8 +255,6 @@ class TaskManager { this.renderHabits(); } else if (tabName === 'finances') { this.renderFinances(); - } else if (tabName === 'shop') { - this.renderShop(); } else if (tabName === 'wishlist') { this.renderWishList(); } else if (tabName === 'notes') { @@ -296,7 +287,6 @@ class TaskManager { const userStats = storage.getUserStats(); // Update header stats - document.getElementById('totalPoints')!.textContent = String(userStats.totalPoints); document.getElementById('userLevel')!.textContent = String(userStats.level); document.getElementById('dailyStreak')!.textContent = String(userStats.dailyStreak); @@ -671,7 +661,6 @@ class TaskManager { ${task.dueDate ? `📅 ${task.dueDate}` : ''} ${task.priority} ${task.repeatType !== 'none' ? `🔁 ${task.repeatType}` : ''} - ${task.points ? `⭐ ${task.points} pts` : ''}
${status}
@@ -715,7 +704,6 @@ class TaskManager { (document.getElementById('taskDueDate') as HTMLInputElement).value = task.dueDate || ''; (document.getElementById('taskCategory') as HTMLSelectElement).value = task.category || ''; (document.getElementById('taskPriority') as HTMLSelectElement).value = task.priority || 'medium'; - (document.getElementById('taskPoints') as HTMLInputElement).value = String(task.points || 10); (document.getElementById('taskRepeatType') as HTMLSelectElement).value = task.repeatType || 'none'; (document.getElementById('taskProject') as HTMLSelectElement).value = task.projectId || ''; (document.getElementById('taskRepeatUnit') as HTMLInputElement).value = String(task.repeatUnit || 1); @@ -809,7 +797,6 @@ class TaskManager { dueDate: (document.getElementById('taskDueDate') as HTMLInputElement).value, category: (document.getElementById('taskCategory') as HTMLSelectElement).value, priority: (document.getElementById('taskPriority') as HTMLSelectElement).value as Task['priority'], - points: parseInt((document.getElementById('taskPoints') as HTMLInputElement).value), repeatType: (document.getElementById('taskRepeatType') as HTMLSelectElement).value as Task['repeatType'], projectId: (document.getElementById('taskProject') as HTMLSelectElement).value || null }; @@ -857,7 +844,6 @@ class TaskManager { task.completed = !task.completed; if (task.completed) { task.completedDate = this.getSelectedDateStr(); - storage.addPoints(task.points, 'tasks'); storage.updateDailyStreak(true); // If repeatable, immediately create next task with recalculated due date if (task.repeatType !== 'none') { @@ -931,7 +917,6 @@ class TaskManager { description: completedTask.description, category: completedTask.category, priority: completedTask.priority, - points: completedTask.points, projectId: completedTask.projectId, repeatType: completedTask.repeatType, repeatUnit: completedTask.repeatUnit, @@ -1184,10 +1169,6 @@ class TaskManager { Streak ${habit.streak || 0} -
- Points - ${habit.points} -
Progress ${todaysCompletions}/${targetGoal} @@ -1231,7 +1212,6 @@ class TaskManager { (document.getElementById('habitIcon') as HTMLInputElement).value = habit.icon || '⭐'; document.getElementById('habitIconDisplay')!.textContent = habit.icon || '⭐'; (document.getElementById('habitCategory') as HTMLSelectElement).value = habit.category || ''; - (document.getElementById('habitPoints') as HTMLInputElement).value = String(habit.points || 5); (document.getElementById('habitTargetGoal') as HTMLInputElement).value = String(habit.targetGoal || 1); deleteBtn.style.display = 'block'; @@ -1274,7 +1254,6 @@ class TaskManager { description: (document.getElementById('habitDescription') as HTMLTextAreaElement).value, icon: (document.getElementById('habitIcon') as HTMLInputElement).value, category: (document.getElementById('habitCategory') as HTMLSelectElement).value || null, - points: parseInt((document.getElementById('habitPoints') as HTMLInputElement).value), targetGoal: parseInt((document.getElementById('habitTargetGoal') as HTMLInputElement).value) || 1, daysOfWeek: selectedDays.length > 0 ? selectedDays : [0, 1, 2, 3, 4, 5, 6] }; @@ -1307,7 +1286,6 @@ class TaskManager { if (isValidDay) { storage.logHabitCompletion(habitId, this.selectedDate); - storage.addPoints(habit.points, 'habits'); storage.updateDailyStreak(true); this.renderHabits(); this.renderDashboard(); @@ -1730,149 +1708,6 @@ class TaskManager { } } - // ======================== - // Shop/Rewards Management - // ======================== - renderShop(): void { - const rewards = storage.getRewards(); - const userStats = storage.getUserStats(); - const container = document.getElementById('rewardsList')!; - - document.getElementById('shopPointsDisplay')!.textContent = String(userStats.totalPoints); - - if (rewards.length === 0) { - container.innerHTML = '

No rewards yet. Add rewards to spend your points on!

'; - return; - } - - container.innerHTML = rewards.map(reward => { - let alreadyPurchased = false; - if (reward.repeatable === false) { - const purchaseHistory = storage.getData().purchaseHistory || []; - alreadyPurchased = purchaseHistory.some(ph => ph.rewardId === reward.id); - } - const disabled = userStats.totalPoints < reward.cost || alreadyPurchased; - let purchaseLabel = 'Purchase'; - if (userStats.totalPoints < reward.cost) purchaseLabel = 'Not Enough Points'; - if (alreadyPurchased) purchaseLabel = 'Purchased'; - - return ` -
-
${reward.name}
- ${reward.description ? `
${reward.description}
` : ''} -
-
- Cost - ${reward.cost} pts -
-
- ${reward.repeatable === false ? 'One-time' : 'Repeatable'} -
-
-
- - -
-
- `; - }).join(''); - - document.querySelectorAll('.purchase-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const card = (e.target as HTMLElement).closest('[data-reward-id]') as HTMLElement; - this.purchaseReward(card.dataset.rewardId!); - }); - }); - - document.querySelectorAll('.edit-reward-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const card = (e.target as HTMLElement).closest('[data-reward-id]') as HTMLElement; - this.openRewardModal(card.dataset.rewardId!); - }); - }); - } - - openRewardModal(rewardId: string | null = null): void { - this.currentEditingRewardId = rewardId; - const modal = document.getElementById('rewardModal')!; - const form = document.getElementById('rewardForm') as HTMLFormElement; - const deleteBtn = document.getElementById('deleteRewardBtn') as HTMLElement; - - form.reset(); - deleteBtn.style.display = 'none'; - - document.getElementById('rewardModalTitle')!.textContent = rewardId ? 'Edit Reward' : 'Add Reward'; - - if (rewardId) { - const reward = storage.getRewards().find(r => r.id === rewardId); - if (reward) { - (document.getElementById('rewardName') as HTMLInputElement).value = reward.name; - (document.getElementById('rewardDescription') as HTMLTextAreaElement).value = reward.description || ''; - (document.getElementById('rewardCost') as HTMLInputElement).value = String(reward.cost); - (document.getElementById('rewardRepeatable') as HTMLSelectElement).value = String(reward.repeatable === undefined ? true : reward.repeatable); - deleteBtn.style.display = 'block'; - } - } - - modal.classList.add('active'); - } - - closeRewardModal(): void { - document.getElementById('rewardModal')!.classList.remove('active'); - this.currentEditingRewardId = null; - } - - saveReward(e: Event): void { - e.preventDefault(); - - const reward = { - name: (document.getElementById('rewardName') as HTMLInputElement).value, - description: (document.getElementById('rewardDescription') as HTMLTextAreaElement).value, - cost: parseInt((document.getElementById('rewardCost') as HTMLInputElement).value), - repeatable: (document.getElementById('rewardRepeatable') as HTMLSelectElement).value === 'true' - }; - - if (this.currentEditingRewardId) { - storage.updateReward(this.currentEditingRewardId, reward); - } else { - storage.addReward(reward); - } - - this.closeRewardModal(); - this.renderShop(); - } - - deleteReward(): void { - if (this.currentEditingRewardId) { - if (confirm('Are you sure you want to delete this reward?')) { - storage.deleteReward(this.currentEditingRewardId); - this.closeRewardModal(); - this.renderShop(); - } - } - } - - purchaseReward(rewardId: string): void { - const reward = storage.getRewards().find(r => r.id === rewardId); - if (!reward) return; - - if (confirm(`Purchase "${reward.name}" for ${reward.cost} points?`)) { - const result = storage.purchaseReward(rewardId); - - if (result.success) { - alert(`Congratulations! You've purchased: ${reward.name}!\n\nEnjoy your reward! 🎉`); - this.renderShop(); - this.renderDashboard(); - } else { - alert(result.message); - } - } - } - // ======================== // Wish List // ======================== diff --git a/src/storage.ts b/src/storage.ts index f0c033f..ef0fa53 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -4,7 +4,7 @@ const STORAGE_VERSION = '1.0.0'; const STORAGE_KEY = 'taskManagerData'; -const DATA_SCHEMA_VERSION = 1; +const DATA_SCHEMA_VERSION = 2; // ======================== // Type Definitions @@ -17,7 +17,6 @@ export interface Task { dueDate?: string; category?: string | null; priority: 'low' | 'medium' | 'high'; - points: number; repeatType: 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom' | 'movable'; repeatUnit?: number; customRepeatDays?: number; @@ -43,7 +42,6 @@ export interface Habit { description?: string; icon: string; category?: string | null; - points: number; targetGoal: number; daysOfWeek?: number[]; streak: number; @@ -68,25 +66,6 @@ export interface FinanceItem { createdDate: string; } -export interface Reward { - id: string; - name: string; - description?: string; - cost: number; - repeatable: boolean; - purchased: boolean; - createdDate: string; -} - -export interface Purchase { - id: string; - rewardId: string; - rewardName: string; - rewardDescription?: string; - cost: number; - purchaseDate: string; -} - export interface WishItem { id: string; title: string; @@ -105,20 +84,15 @@ export interface Note { updatedDate?: string; } -export interface PointsBreakdown { - tasks: number; - projects: number; - habits: number; - streakBonus: number; - [key: string]: number; -} - export interface UserStats { - totalPoints: number; level: number; dailyStreak: number; lastActivityDate: string | null; - pointsBreakdown: PointsBreakdown; +} + +interface LegacyUserStats extends UserStats { + totalPoints?: number; + pointsBreakdown?: Record; } export interface Settings { @@ -136,8 +110,6 @@ export interface AppData { expenses: FinanceItem[]; revenue: FinanceItem[]; charges: FinanceItem[]; - rewards: Reward[]; - purchaseHistory: Purchase[]; categories: string[]; userStats: UserStats; settings: Settings; @@ -145,12 +117,6 @@ export interface AppData { notes: Note[]; } -export interface PurchaseResult { - success: boolean; - message?: string; - purchase?: Purchase; -} - export interface ValidationResult { isValid: boolean; hasPartialData: boolean; @@ -167,6 +133,16 @@ export class StorageManager { const existingData = localStorage.getItem(STORAGE_KEY); if (!existingData) { this.createInitialData(); + } else { + try { + const data = JSON.parse(existingData) as Partial; + if (!data.schemaVersion || data.schemaVersion < DATA_SCHEMA_VERSION) { + this.migrateToLatest(); + } + } catch (e) { + console.error('Failed to read or migrate existing data:', e); + this.createInitialData(); + } } } @@ -182,20 +158,11 @@ export class StorageManager { expenses: [], revenue: [], charges: [], - rewards: [], - purchaseHistory: [], categories: ['Work', 'Personal', 'Home', 'Shopping', 'Health', 'Fitness', 'Learning', 'Productivity', 'Food', 'Transportation', 'Entertainment', 'Utilities', 'Income'], userStats: { - totalPoints: 0, level: 1, dailyStreak: 0, - lastActivityDate: null, - pointsBreakdown: { - tasks: 0, - projects: 0, - habits: 0, - streakBonus: 0 - } + lastActivityDate: null }, settings: { tasksPerLevel: 30 @@ -232,7 +199,6 @@ export class StorageManager { completed: false, title: task.title || '', priority: task.priority || 'medium', - points: task.points || 10, repeatType: task.repeatType || 'none', } as Task; data.tasks.push(newTask); @@ -315,7 +281,6 @@ export class StorageManager { targetGoal: habit.targetGoal || 1, name: habit.name || '', icon: habit.icon || '⭐', - points: habit.points || 10, } as Habit; data.habits.push(newHabit); this.saveData(data); @@ -535,103 +500,6 @@ export class StorageManager { return data.charges || []; } - // Rewards Shop Management - addReward(reward: Partial): Reward { - const data = this.getData(); - if (!data.rewards) { - data.rewards = []; - } - const newReward: Reward = { - ...reward, - id: this.generateId(), - createdDate: new Date().toISOString(), - purchased: false, - repeatable: typeof reward.repeatable === 'undefined' ? true : reward.repeatable, - name: reward.name || '', - cost: reward.cost || 0, - } as Reward; - data.rewards.push(newReward); - this.saveData(data); - return newReward; - } - - updateReward(rewardId: string, updates: Partial): Reward | undefined { - const data = this.getData(); - if (!data.rewards) { - data.rewards = []; - } - const reward = data.rewards.find(r => r.id === rewardId); - if (reward) { - Object.assign(reward, updates); - if (typeof reward.repeatable === 'undefined') reward.repeatable = true; - this.saveData(data); - } - return reward; - } - - deleteReward(rewardId: string): void { - const data = this.getData(); - if (!data.rewards) { - data.rewards = []; - } - data.rewards = data.rewards.filter(r => r.id !== rewardId); - this.saveData(data); - } - - getRewards(): Reward[] { - const data = this.getData(); - return data.rewards || []; - } - - purchaseReward(rewardId: string): PurchaseResult { - const data = this.getData(); - if (!data.rewards) { - data.rewards = []; - } - if (!data.purchaseHistory) { - data.purchaseHistory = []; - } - - const reward = data.rewards.find(r => r.id === rewardId); - if (!reward) { - return { success: false, message: 'Reward not found' }; - } - - if (data.userStats.totalPoints < reward.cost) { - return { success: false, message: 'Not enough points' }; - } - - // If one-time and already purchased, block - if (reward.repeatable === false) { - const alreadyPurchased = data.purchaseHistory.some(ph => ph.rewardId === reward.id); - if (alreadyPurchased) { - return { success: false, message: 'This reward can only be purchased once.' }; - } - } - - // Deduct points - data.userStats.totalPoints -= reward.cost; - - // Add to purchase history - const purchase: Purchase = { - id: this.generateId(), - rewardId: reward.id, - rewardName: reward.name, - rewardDescription: reward.description, - cost: reward.cost, - purchaseDate: new Date().toISOString() - }; - data.purchaseHistory.push(purchase); - - this.saveData(data); - return { success: true, purchase }; - } - - getPurchaseHistory(): Purchase[] { - const data = this.getData(); - return data.purchaseHistory || []; - } - // Wish List Management addWishItem(item: Partial): WishItem { const data = this.getData(); @@ -726,18 +594,6 @@ export class StorageManager { ); } - // Points Management - addPoints(amount: number, source: string): void { - const data = this.getData(); - data.userStats.totalPoints += amount; - if (data.userStats.pointsBreakdown[source] !== undefined) { - data.userStats.pointsBreakdown[source] += amount; - } - // Level is now calculated based on completed tasks, not points - this.updateLevel(); - this.saveData(data); - } - updateLevel(): void { const data = this.getData(); const settings = this.getSettings(); @@ -855,15 +711,11 @@ export class StorageManager { expenses: Array.isArray(data.expenses) ? data.expenses : [], revenue: Array.isArray(data.revenue) ? data.revenue : [], charges: Array.isArray(data.charges) ? data.charges : [], - rewards: Array.isArray(data.rewards) ? data.rewards : [], - purchaseHistory: Array.isArray(data.purchaseHistory) ? data.purchaseHistory : [], categories, - userStats: data.userStats || { - totalPoints: 0, - level: 1, - dailyStreak: 0, - lastActivityDate: null, - pointsBreakdown: { tasks: 0, projects: 0, habits: 0, streakBonus: 0 } + userStats: { + level: (data.userStats as LegacyUserStats)?.level ?? 1, + dailyStreak: (data.userStats as LegacyUserStats)?.dailyStreak ?? 0, + lastActivityDate: (data.userStats as LegacyUserStats)?.lastActivityDate ?? null, }, settings: data.settings || { tasksPerLevel: 30 }, wishList: Array.isArray(data.wishList) ? data.wishList : [], diff --git a/tests/storage.test.ts b/tests/storage.test.ts index 678dbcb..3681e0c 100644 --- a/tests/storage.test.ts +++ b/tests/storage.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { StorageManager, getDaysUntilDueText } from '../src/storage'; -import type { Task, Habit, FinanceItem, Reward } from '../src/storage'; +import type { Task, Habit, FinanceItem } from '../src/storage'; // Mock localStorage const localStorageMock = (() => { @@ -42,7 +42,6 @@ describe('StorageManager', () => { expect(data.projects).toEqual([]); expect(data.habits).toEqual([]); expect(data.categories).toContain('Work'); - expect(data.userStats.totalPoints).toBe(0); expect(data.userStats.level).toBe(1); }); @@ -58,11 +57,10 @@ describe('StorageManager', () => { // ======================== describe('task management', () => { it('should add a task', () => { - const task = storage.addTask({ title: 'Buy groceries', priority: 'high', points: 15 }); + const task = storage.addTask({ title: 'Buy groceries', priority: 'high' }); expect(task.id).toBeDefined(); expect(task.title).toBe('Buy groceries'); expect(task.priority).toBe('high'); - expect(task.points).toBe(15); expect(task.completed).toBe(false); expect(task.createdDate).toBeDefined(); }); @@ -94,7 +92,6 @@ describe('StorageManager', () => { it('should set default values for task fields', () => { const task = storage.addTask({ title: 'Minimal' }); expect(task.priority).toBe('medium'); - expect(task.points).toBe(10); expect(task.repeatType).toBe('none'); }); }); @@ -137,7 +134,7 @@ describe('StorageManager', () => { // ======================== describe('habit management', () => { it('should add a habit', () => { - const habit = storage.addHabit({ name: 'Exercise', icon: '💪', points: 20 }); + const habit = storage.addHabit({ name: 'Exercise', icon: '💪' }); expect(habit.id).toBeDefined(); expect(habit.name).toBe('Exercise'); expect(habit.streak).toBe(0); @@ -335,81 +332,9 @@ describe('StorageManager', () => { }); // ======================== - // Rewards Shop + // Leveling & Streak // ======================== - describe('rewards shop', () => { - it('should add a reward', () => { - const reward = storage.addReward({ name: 'Movie Night', cost: 100 }); - expect(reward.id).toBeDefined(); - expect(reward.name).toBe('Movie Night'); - expect(reward.repeatable).toBe(true); - expect(reward.purchased).toBe(false); - }); - - it('should update a reward', () => { - const reward = storage.addReward({ name: 'Movie', cost: 100 }); - storage.updateReward(reward.id, { name: 'Movie Night', cost: 150 }); - const updated = storage.getRewards().find(r => r.id === reward.id); - expect(updated?.name).toBe('Movie Night'); - expect(updated?.cost).toBe(150); - }); - - it('should delete a reward', () => { - const reward = storage.addReward({ name: 'Movie Night', cost: 100 }); - storage.deleteReward(reward.id); - expect(storage.getRewards().length).toBe(0); - }); - - it('should purchase a reward and deduct points', () => { - storage.addPoints(200, 'tasks'); - const reward = storage.addReward({ name: 'Movie Night', cost: 100 }); - const result = storage.purchaseReward(reward.id); - expect(result.success).toBe(true); - expect(storage.getUserStats().totalPoints).toBe(100); - expect(storage.getPurchaseHistory().length).toBe(1); - }); - - it('should fail to purchase with insufficient points', () => { - const reward = storage.addReward({ name: 'Expensive', cost: 9999 }); - const result = storage.purchaseReward(reward.id); - expect(result.success).toBe(false); - expect(result.message).toBe('Not enough points'); - }); - - it('should prevent re-purchasing one-time rewards', () => { - storage.addPoints(500, 'tasks'); - const reward = storage.addReward({ name: 'One-time', cost: 100, repeatable: false }); - storage.purchaseReward(reward.id); - const result = storage.purchaseReward(reward.id); - expect(result.success).toBe(false); - expect(result.message).toBe('This reward can only be purchased once.'); - }); - - it('should allow re-purchasing repeatable rewards', () => { - storage.addPoints(500, 'tasks'); - const reward = storage.addReward({ name: 'Repeatable', cost: 100, repeatable: true }); - storage.purchaseReward(reward.id); - const result = storage.purchaseReward(reward.id); - expect(result.success).toBe(true); - }); - - it('should fail for non-existent reward', () => { - const result = storage.purchaseReward('nonexistent'); - expect(result.success).toBe(false); - expect(result.message).toBe('Reward not found'); - }); - }); - - // ======================== - // Points & Leveling - // ======================== - describe('points and leveling', () => { - it('should add points', () => { - storage.addPoints(50, 'tasks'); - expect(storage.getUserStats().totalPoints).toBe(50); - expect(storage.getUserStats().pointsBreakdown.tasks).toBe(50); - }); - + describe('leveling and streak', () => { it('should calculate level based on completed tasks', () => { // Default: 30 tasks per level for (let i = 0; i < 31; i++) {