@@ -44,8 +44,15 @@ jest.mock('../tasks-utils', () => {
4444} ) ;
4545
4646jest . mock ( '@/components/ui/multi-select' , ( ) => ( {
47- MultiSelectFilter : jest . fn ( ( { title } ) => (
48- < div > Mocked MultiSelect: { title } </ div >
47+ MultiSelectFilter : jest . fn ( ( { title, completionStats } ) => (
48+ < div data-testid = { `multi-select-${ title . toLowerCase ( ) } ` } >
49+ Mocked MultiSelect: { title }
50+ { completionStats && (
51+ < span data-testid = { `stats-${ title . toLowerCase ( ) } ` } >
52+ { JSON . stringify ( completionStats ) }
53+ </ span >
54+ ) }
55+ </ div >
4956 ) ) ,
5057} ) ) ;
5158
@@ -1364,6 +1371,155 @@ describe('Tasks Component', () => {
13641371 } ) ;
13651372 } ) ;
13661373
1374+ test ( 'calculates and passes project completion stats to MultiSelectFilter' , async ( ) => {
1375+ render ( < Tasks { ...mockProps } /> ) ;
1376+
1377+ await waitFor ( async ( ) => {
1378+ expect ( await screen . findByText ( 'Task 1' ) ) . toBeInTheDocument ( ) ;
1379+ } ) ;
1380+
1381+ const { MultiSelectFilter } = require ( '@/components/ui/multi-select' ) ;
1382+
1383+ // Find the Projects filter call
1384+ const projectsFilterCall = MultiSelectFilter . mock . calls . find (
1385+ ( call : any ) => call [ 0 ] . title === 'Projects'
1386+ ) ;
1387+
1388+ expect ( projectsFilterCall ) . toBeDefined ( ) ;
1389+ expect ( projectsFilterCall [ 0 ] . completionStats ) . toBeDefined ( ) ;
1390+
1391+ const stats = projectsFilterCall [ 0 ] . completionStats ;
1392+
1393+ // ProjectA has tasks: 1,3,5,7,9,11 (pending) + task 16 (completed) = 1 completed out of 7 total
1394+ expect ( stats [ 'ProjectA' ] ) . toBeDefined ( ) ;
1395+ expect ( stats [ 'ProjectA' ] . completed ) . toBeGreaterThanOrEqual ( 1 ) ;
1396+ expect ( stats [ 'ProjectA' ] . total ) . toBeGreaterThanOrEqual ( 1 ) ;
1397+ expect ( stats [ 'ProjectA' ] . percentage ) . toBeGreaterThanOrEqual ( 0 ) ;
1398+ expect ( stats [ 'ProjectA' ] . percentage ) . toBeLessThanOrEqual ( 100 ) ;
1399+
1400+ // ProjectB has tasks: 2,4,6,8,10,12 (pending) + task 17 (deleted) = 0 completed
1401+ expect ( stats [ 'ProjectB' ] ) . toBeDefined ( ) ;
1402+ expect ( stats [ 'ProjectB' ] . total ) . toBeGreaterThanOrEqual ( 1 ) ;
1403+ } ) ;
1404+
1405+ test ( 'calculates and passes tag completion stats to MultiSelectFilter' , async ( ) => {
1406+ render ( < Tasks { ...mockProps } /> ) ;
1407+
1408+ await waitFor ( async ( ) => {
1409+ expect ( await screen . findByText ( 'Task 1' ) ) . toBeInTheDocument ( ) ;
1410+ } ) ;
1411+
1412+ const { MultiSelectFilter } = require ( '@/components/ui/multi-select' ) ;
1413+
1414+ // Find the Tags filter call
1415+ const tagsFilterCall = MultiSelectFilter . mock . calls . find (
1416+ ( call : any ) => call [ 0 ] . title === 'Tags'
1417+ ) ;
1418+
1419+ expect ( tagsFilterCall ) . toBeDefined ( ) ;
1420+ expect ( tagsFilterCall [ 0 ] . completionStats ) . toBeDefined ( ) ;
1421+
1422+ const stats = tagsFilterCall [ 0 ] . completionStats ;
1423+
1424+ // Verify stats structure
1425+ Object . keys ( stats ) . forEach ( ( tag ) => {
1426+ expect ( stats [ tag ] ) . toHaveProperty ( 'completed' ) ;
1427+ expect ( stats [ tag ] ) . toHaveProperty ( 'total' ) ;
1428+ expect ( stats [ tag ] ) . toHaveProperty ( 'percentage' ) ;
1429+ expect ( typeof stats [ tag ] . completed ) . toBe ( 'number' ) ;
1430+ expect ( typeof stats [ tag ] . total ) . toBe ( 'number' ) ;
1431+ expect ( typeof stats [ tag ] . percentage ) . toBe ( 'number' ) ;
1432+ expect ( stats [ tag ] . percentage ) . toBeGreaterThanOrEqual ( 0 ) ;
1433+ expect ( stats [ tag ] . percentage ) . toBeLessThanOrEqual ( 100 ) ;
1434+ } ) ;
1435+ } ) ;
1436+
1437+ test ( 'recalculates completion stats after sync' , async ( ) => {
1438+ const hooks = require ( '../hooks' ) ;
1439+
1440+ render ( < Tasks { ...mockProps } /> ) ;
1441+
1442+ await waitFor ( async ( ) => {
1443+ expect ( await screen . findByText ( 'Task 1' ) ) . toBeInTheDocument ( ) ;
1444+ } ) ;
1445+
1446+ const { MultiSelectFilter } = require ( '@/components/ui/multi-select' ) ;
1447+
1448+ hooks . fetchTaskwarriorTasks . mockResolvedValueOnce ( [
1449+ {
1450+ id : 1 ,
1451+ description : 'Task 1' ,
1452+ status : 'completed' ,
1453+ project : 'ProjectA' ,
1454+ tags : [ 'tag1' ] ,
1455+ uuid : 'uuid-1' ,
1456+ } ,
1457+ {
1458+ id : 2 ,
1459+ description : 'Task 2' ,
1460+ status : 'completed' ,
1461+ project : 'ProjectB' ,
1462+ tags : [ 'tag2' ] ,
1463+ uuid : 'uuid-2' ,
1464+ } ,
1465+ ] ) ;
1466+
1467+ MultiSelectFilter . mockClear ( ) ;
1468+
1469+ const syncButtons = screen . getAllByText ( 'Sync' ) ;
1470+ fireEvent . click ( syncButtons [ 0 ] ) ;
1471+
1472+ await waitFor ( ( ) => {
1473+ const projectsCall = MultiSelectFilter . mock . calls . find (
1474+ ( call : any ) => call [ 0 ] . title === 'Projects'
1475+ ) ;
1476+ expect ( projectsCall ) . toBeDefined ( ) ;
1477+ } ) ;
1478+
1479+ const updatedProjectsCall = MultiSelectFilter . mock . calls . find (
1480+ ( call : any ) => call [ 0 ] . title === 'Projects'
1481+ ) ;
1482+
1483+ expect ( updatedProjectsCall ) . toBeDefined ( ) ;
1484+ expect ( updatedProjectsCall [ 0 ] . completionStats ) . toBeDefined ( ) ;
1485+
1486+ const updatedStats = updatedProjectsCall [ 0 ] . completionStats ;
1487+ expect ( updatedStats [ 'ProjectA' ] ) . toBeDefined ( ) ;
1488+ expect ( updatedStats [ 'ProjectB' ] ) . toBeDefined ( ) ;
1489+ } ) ;
1490+
1491+ test ( 'completion stats structure is correct' , async ( ) => {
1492+ render ( < Tasks { ...mockProps } /> ) ;
1493+
1494+ await waitFor ( async ( ) => {
1495+ expect ( await screen . findByText ( 'Task 1' ) ) . toBeInTheDocument ( ) ;
1496+ } ) ;
1497+
1498+ const { MultiSelectFilter } = require ( '@/components/ui/multi-select' ) ;
1499+
1500+ const projectsCall = MultiSelectFilter . mock . calls . find (
1501+ ( call : any ) => call [ 0 ] . title === 'Projects'
1502+ ) ;
1503+
1504+ expect ( projectsCall ) . toBeDefined ( ) ;
1505+ const stats = projectsCall [ 0 ] . completionStats ;
1506+
1507+ // Verify stats structure for any project that exists
1508+ Object . keys ( stats ) . forEach ( ( project ) => {
1509+ expect ( stats [ project ] ) . toHaveProperty ( 'completed' ) ;
1510+ expect ( stats [ project ] ) . toHaveProperty ( 'total' ) ;
1511+ expect ( stats [ project ] ) . toHaveProperty ( 'percentage' ) ;
1512+ expect ( typeof stats [ project ] . completed ) . toBe ( 'number' ) ;
1513+ expect ( typeof stats [ project ] . total ) . toBe ( 'number' ) ;
1514+ expect ( typeof stats [ project ] . percentage ) . toBe ( 'number' ) ;
1515+ expect ( stats [ project ] . completed ) . toBeLessThanOrEqual (
1516+ stats [ project ] . total
1517+ ) ;
1518+ expect ( stats [ project ] . percentage ) . toBeGreaterThanOrEqual ( 0 ) ;
1519+ expect ( stats [ project ] . percentage ) . toBeLessThanOrEqual ( 100 ) ;
1520+ } ) ;
1521+ } ) ;
1522+
13671523 describe ( 'Pin Functionality' , ( ) => {
13681524 test ( 'should load pinned tasks from localStorage on mount' , async ( ) => {
13691525 const { getPinnedTasks } = require ( '../tasks-utils' ) ;
0 commit comments