11import { beforeEach , describe , it , expect } from 'vitest' ;
22import { render , screen } from '@testing-library/react' ;
3- import { TimelineView } from './TimelineView' ;
3+ import { TimelineView , resolveTimelineTrack } from './TimelineView' ;
44import { LanguageProvider } from '../../contexts/language-context' ;
55import { useTaskStore , type Area , type Project , type Task } from '@mindwtr/core' ;
66
@@ -42,6 +42,26 @@ const renderTimeline = () => render(
4242
4343const bars = ( ) => Array . from ( document . querySelectorAll ( '[data-testid="timeline-bar"]' ) ) as HTMLElement [ ] ;
4444const barFor = ( taskId : string ) => document . querySelector ( `[data-testid="timeline-bar"][data-task-id="${ taskId } "]` ) as HTMLElement | null ;
45+ // Group headings and bar titles in one pass, in the order they are laid out.
46+ const rowLabels = ( ) => Array . from (
47+ document . querySelectorAll ( '[data-testid="timeline-group"], [data-testid="timeline-bar"]' ) ,
48+ ) . map ( ( node ) => node . textContent ) ;
49+ const axisLabels = ( tier : 'major' | 'minor' ) => Array . from (
50+ document . querySelectorAll ( `[data-testid="timeline-axis-${ tier } "]` ) ,
51+ ) . map ( ( node ) => node . textContent ?? '' ) ;
52+
53+ describe ( 'resolveTimelineTrack (#1111)' , ( ) => {
54+ it ( 'stretches a range that fits the pane and scrolls one that does not' , ( ) => {
55+ // 30 days at the month zoom's 4px minimum is 120px in a 1200px pane.
56+ expect ( resolveTimelineTrack ( 30 , 4 , 1200 ) ) . toEqual ( { dayWidth : 40 , trackWidth : 1200 , fitted : true } ) ;
57+ // 400 days at 4px overflows, so the minimum stands and the track scrolls.
58+ expect ( resolveTimelineTrack ( 400 , 4 , 1200 ) ) . toEqual ( { dayWidth : 4 , trackWidth : 1600 , fitted : false } ) ;
59+ // Exactly full is still fitted, and the pre-measure paint uses the minimum.
60+ expect ( resolveTimelineTrack ( 100 , 12 , 1200 ) . fitted ) . toBe ( true ) ;
61+ expect ( resolveTimelineTrack ( 30 , 4 , 0 ) ) . toEqual ( { dayWidth : 4 , trackWidth : 120 , fitted : false } ) ;
62+ expect ( resolveTimelineTrack ( 0 , 4 , 1200 ) ) . toEqual ( { dayWidth : 4 , trackWidth : 0 , fitted : false } ) ;
63+ } ) ;
64+ } ) ;
4565
4666describe ( 'TimelineView (#1111)' , ( ) => {
4767 beforeEach ( ( ) => {
@@ -71,7 +91,7 @@ describe('TimelineView (#1111)', () => {
7191 renderTimeline ( ) ;
7292 expect ( barFor ( 'start-only' ) ?. dataset . variant ) . toBe ( 'mini' ) ;
7393 expect ( barFor ( 'due-only' ) ?. dataset . variant ) . toBe ( 'mini' ) ;
74- expect ( barFor ( 'start-only' ) ?. style . width ) . toBe ( '10px ' ) ;
94+ expect ( barFor ( 'start-only' ) ?. style . width ) . toBe ( '56px ' ) ;
7595 } ) ;
7696
7797 it ( 'leaves out undated, done and deleted tasks' , ( ) => {
@@ -87,11 +107,14 @@ describe('TimelineView (#1111)', () => {
87107 expect ( bars ( ) . map ( ( bar ) => bar . dataset . taskId ) ) . toEqual ( [ 'dated' ] ) ;
88108 } ) ;
89109
90- it ( " colors a bar with its project's area color, falling back to the project color" , ( ) => {
110+ it ( ' colors a bar with the same accent the calendar gives that task' , ( ) => {
91111 setStore ( {
92112 tasks : [
93113 makeTask ( { id : 'in-area' , title : 'In area' , projectId : 'p1' , startTime : iso ( 0 ) , dueDate : iso ( 1 ) } ) ,
94114 makeTask ( { id : 'plain' , title : 'Plain' , projectId : 'p2' , startTime : iso ( 0 ) , dueDate : iso ( 1 ) } ) ,
115+ // No project, area straight on the task: colored on the calendar,
116+ // so colored here too.
117+ makeTask ( { id : 'loose' , title : 'Loose' , areaId : 'a1' , startTime : iso ( 0 ) , dueDate : iso ( 1 ) } ) ,
95118 ] ,
96119 projects : [
97120 { id : 'p1' , title : 'Area project' , status : 'active' , areaId : 'a1' , createdAt : iso ( - 60 ) , updatedAt : iso ( - 60 ) } as Project ,
@@ -102,6 +125,7 @@ describe('TimelineView (#1111)', () => {
102125 renderTimeline ( ) ;
103126 expect ( barFor ( 'in-area' ) ?. style . backgroundColor ) . toBe ( 'rgb(255, 0, 0)' ) ;
104127 expect ( barFor ( 'plain' ) ?. style . backgroundColor ) . toBe ( 'rgb(0, 255, 0)' ) ;
128+ expect ( barFor ( 'loose' ) ?. style . backgroundColor ) . toBe ( 'rgb(255, 0, 0)' ) ;
105129 } ) ;
106130
107131 it ( 'groups rows by project with unassigned tasks last' , ( ) => {
@@ -113,8 +137,27 @@ describe('TimelineView (#1111)', () => {
113137 projects : [ { id : 'p1' , title : 'Area project' , status : 'active' , createdAt : iso ( - 60 ) , updatedAt : iso ( - 60 ) } as Project ] ,
114138 } ) ;
115139 renderTimeline ( ) ;
116- const labels = Array . from ( document . querySelectorAll ( 'span.truncate' ) ) . map ( ( node ) => node . textContent ) ;
117- expect ( labels ) . toEqual ( [ 'Area project' , 'Owned task' , 'No project' , 'Loose task' ] ) ;
140+ expect ( rowLabels ( ) ) . toEqual ( [ 'Area project' , 'Owned task' , 'No project' , 'Loose task' ] ) ;
141+ } ) ;
142+
143+ it ( 'splits the month-zoom axis into a year tier and month ticks, and floors thin bars' , ( ) => {
144+ // The shipped axis printed "MMM yyyy" on every month start, which
145+ // collided at 4px per day; the year moves to the top tier instead.
146+ window . localStorage . setItem ( 'mindwtr:view:timeline:v1' , JSON . stringify ( { zoom : 'month' } ) ) ;
147+ setStore ( {
148+ tasks : [
149+ makeTask ( { id : 'long' , title : 'Long haul' , startTime : iso ( - 60 ) , dueDate : iso ( 90 ) } ) ,
150+ makeTask ( { id : 'oneday' , title : 'One day' , startTime : iso ( 5 ) , dueDate : iso ( 5 ) } ) ,
151+ ] ,
152+ } ) ;
153+ renderTimeline ( ) ;
154+
155+ expect ( axisLabels ( 'major' ) . every ( ( label ) => / ^ \d { 4 } $ / . test ( label ) ) ) . toBe ( true ) ;
156+ const minor = axisLabels ( 'minor' ) ;
157+ expect ( minor . length ) . toBeGreaterThan ( 2 ) ;
158+ expect ( minor . every ( ( label ) => / ^ [ A - Z a - z ] + $ / . test ( label ) ) ) . toBe ( true ) ;
159+ // One day is 4px at month zoom; a bar never renders as a sliver.
160+ expect ( barFor ( 'oneday' ) ?. style . width ) . toBe ( '10px' ) ;
118161 } ) ;
119162
120163 it ( 'marks today and shows the empty state when nothing is dated' , ( ) => {
0 commit comments