File size: 6,608 Bytes
cd99321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { afterEach, describe, expect, it, vi } from 'vitest'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render } from '../../../tests/helpers/render'
import { resetAllStores } from '../../../tests/helpers/store'
import VacayMonthCard from './VacayMonthCard'

const baseProps = {
  year: 2025,
  month: 0, // January 2025
  holidays: {},
  companyHolidaySet: new Set<string>(),
  companyHolidaysEnabled: true,
  entryMap: {},
  onCellClick: vi.fn(),
  companyMode: false,
  blockWeekends: true,
  weekendDays: [0, 6],
}

afterEach(() => {
  resetAllStores()
  vi.clearAllMocks()
})

describe('VacayMonthCard', () => {
  it('FE-COMP-VACAYMONTHCARD-001: Renders the month name', () => {
    render(<VacayMonthCard {...baseProps} />)
    // January in en-US locale via Intl.DateTimeFormat
    expect(screen.getByText(/january/i)).toBeInTheDocument()
  })

  it('FE-COMP-VACAYMONTHCARD-002: Renders correct number of day cells for January 2025', () => {
    render(<VacayMonthCard {...baseProps} />)
    // January 2025 has 31 days
    for (let d = 1; d <= 31; d++) {
      expect(screen.getByText(String(d))).toBeInTheDocument()
    }
  })

  it('FE-COMP-VACAYMONTHCARD-003: Calls onCellClick with the correct ISO date string', async () => {
    const user = userEvent.setup()
    render(<VacayMonthCard {...baseProps} />)
    // January 15, 2025 is a Wednesday (not blocked)
    await user.click(screen.getByText('15'))
    expect(baseProps.onCellClick).toHaveBeenCalledWith('2025-01-15')
  })

  it('FE-COMP-VACAYMONTHCARD-004: Holiday cell has tooltip with localName', () => {
    const props = {
      ...baseProps,
      holidays: { '2025-01-01': { name: 'Neujahr', localName: 'Neujahr', label: null, color: '#ef4444' } },
    }
    render(<VacayMonthCard {...props} />)
    // Jan 1 is a Wednesday — there may be multiple "1" text nodes, find the one with a title
    const cell = screen.getByTitle('Neujahr')
    expect(cell).toBeInTheDocument()
  })

  it('FE-COMP-VACAYMONTHCARD-005: Holiday cell with label shows combined tooltip', () => {
    const props = {
      ...baseProps,
      holidays: { '2025-01-01': { name: 'New Year', localName: 'New Year', label: 'DE', color: '#ef4444' } },
    }
    render(<VacayMonthCard {...props} />)
    const cell = screen.getByTitle('DE: New Year')
    expect(cell).toBeInTheDocument()
  })

  it('FE-COMP-VACAYMONTHCARD-006: Weekend cell has default cursor (blocked)', () => {
    render(<VacayMonthCard {...baseProps} />)
    // January 5, 2025 is a Sunday (getDay() === 0), which is in weekendDays [0, 6]
    // isBlocked = weekend && blockWeekends = true
    const daySpan = screen.getByText('5')
    const cell = daySpan.closest('div') as HTMLElement
    expect(cell.style.cursor).toBe('default')
  })

  it('FE-COMP-VACAYMONTHCARD-007: Company holiday overlay renders', () => {
    const props = {
      ...baseProps,
      companyHolidaySet: new Set(['2025-01-10']),
      companyHolidaysEnabled: true,
    }
    render(<VacayMonthCard {...props} />)
    // January 10, 2025 is a Friday (not a weekend)
    const daySpan = screen.getByText('10')
    const cell = daySpan.closest('div') as HTMLElement
    // Company overlay is a direct child div with amber background
    const overlayDivs = Array.from(cell.querySelectorAll(':scope > div')) as HTMLElement[]
    const companyOverlay = overlayDivs.find(el => el.className.includes('bg-[rgba(245,158,11'))
    expect(companyOverlay).toBeTruthy()
  })

  it('FE-COMP-VACAYMONTHCARD-008: Single vacation entry renders colored overlay', () => {
    const props = {
      ...baseProps,
      entryMap: { '2025-01-15': [{ date: '2025-01-15', user_id: 1, person_color: '#6366f1' }] },
    }
    render(<VacayMonthCard {...props} />)
    const daySpan = screen.getByText('15')
    const cell = daySpan.closest('div') as HTMLElement
    // The overlay div should have opacity: 0.4 and a backgroundColor set
    const overlayDivs = Array.from(cell.querySelectorAll(':scope > div')) as HTMLElement[]
    const colorOverlay = overlayDivs.find(
      el => el.style.opacity === '0.4' && el.style.backgroundColor !== '',
    )
    expect(colorOverlay).toBeTruthy()
  })

  it('FE-COMP-VACAYMONTHCARD-009: Day number font-weight is bold when entries exist', () => {
    const props = {
      ...baseProps,
      entryMap: { '2025-01-20': [{ date: '2025-01-15', user_id: 1, person_color: '#6366f1' }] },
    }
    render(<VacayMonthCard {...props} />)
    const daySpan = screen.getByText('20')
    expect(daySpan.style.fontWeight).toBe('700')
  })

  it('FE-COMP-VACAYMONTHCARD-010: Renders 7 weekday header labels', () => {
    render(<VacayMonthCard {...baseProps} />)
    // Weekday labels from translations: Mon, Tue, Wed, Thu, Fri, Sat, Sun
    const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    for (const wd of weekdays) {
      expect(screen.getByText(wd)).toBeInTheDocument()
    }
  })

  it('FE-COMP-VACAYMONTHCARD-011: Two vacation entries render gradient overlay', () => {
    const props = {
      ...baseProps,
      entryMap: {
        '2025-01-15': [{ date: '2025-01-15', user_id: 1, person_color: '#6366f1' }, { date: '2025-01-15', user_id: 1, person_color: '#f43f5e' }],
      },
    }
    render(<VacayMonthCard {...props} />)
    const daySpan = screen.getByText('15')
    const cell = daySpan.closest('div') as HTMLElement
    const overlayDivs = Array.from(cell.querySelectorAll(':scope > div')) as HTMLElement[]
    const gradientOverlay = overlayDivs.find(
      el => el.style.opacity === '0.4' && el.style.background.includes('linear-gradient'),
    )
    expect(gradientOverlay).toBeTruthy()
  })

  it('FE-COMP-VACAYMONTHCARD-012: Four vacation entries render quadrant overlay', () => {
    const props = {
      ...baseProps,
      entryMap: {
        '2025-01-15': [
          { date: '2025-01-15', user_id: 1, person_color: '#6366f1' },
          { date: '2025-01-15', user_id: 1, person_color: '#f43f5e' },
          { date: '2025-01-15', user_id: 1, person_color: '#22c55e' },
          { date: '2025-01-15', user_id: 1, person_color: '#f59e0b' },
        ],
      },
    }
    render(<VacayMonthCard {...props} />)
    const daySpan = screen.getByText('15')
    const cell = daySpan.closest('div') as HTMLElement
    // Quadrant overlay wrapper div (4 entries) has 4 sub-divs
    const wrapperDiv = cell.querySelector(':scope > div') as HTMLElement
    expect(wrapperDiv).toBeTruthy()
    const quadrants = wrapperDiv.querySelectorAll(':scope > div')
    expect(quadrants).toHaveLength(4)
  })
})