From ef6a167431faa1fb3dec1d89c69182c270838986 Mon Sep 17 00:00:00 2001 From: zachd Date: Mon, 10 Nov 2025 00:28:42 +0100 Subject: [PATCH 1/2] Enhance calendar functionality with start date management and excluded month visibility. Added start date state and date picker for user-defined start dates. Updated holiday and PTO calculations to respect the start date. Improved UI to toggle excluded months and display active months based on the start date. Refactored related utility functions for better date handling. --- src/lib/CalendarMonth.svelte | 30 ++- src/lib/holidayUtils.ts | 40 +-- src/routes/+page.svelte | 479 +++++++++++++++++++++++++++++++++-- 3 files changed, 503 insertions(+), 46 deletions(-) diff --git a/src/lib/CalendarMonth.svelte b/src/lib/CalendarMonth.svelte index 40e5262..326dc9a 100644 --- a/src/lib/CalendarMonth.svelte +++ b/src/lib/CalendarMonth.svelte @@ -8,6 +8,8 @@ export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>; export let selectedCountryCode: string; export let weekendDays: number[] = [6, 0]; + export let startDate: Date = new Date(year, 0, 1); + export let isActive: boolean = true; // Function to determine the first day of the week based on locale function getFirstDayOfWeek(locale: string): number { @@ -84,12 +86,19 @@ return weekendDays.includes(date.getDay()); } + function isPastDate(day: number): boolean { + const date = new Date(year, month, day); + // Normalize startDate to current year for comparison + const startDateInYear = new Date(year, startDate.getMonth(), startDate.getDate()); + return date < startDateInYear; + } + const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; $: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek)); -
+
{new Date(year, month).toLocaleString('default', { month: 'long' })}
{#each orderedDayInitials as dayInitial} @@ -101,7 +110,8 @@ {/each} {#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day} {@const holiday = getHoliday(day)} -
+ {@const pastDate = isPastDate(day)} +
{day} {#if holiday} @@ -137,6 +147,14 @@ color: #c5c6c7; font-size: 0.6em; } + + .excluded-month .month-name { + color: #666; + } + + .excluded-month .day-initial { + color: #666; + } .day { aspect-ratio: 1; text-align: center; @@ -202,4 +220,12 @@ text-decoration: line-through; opacity: 0.5; } + + .past-date { + opacity: 0.4; + } + + .past-date span { + text-decoration: line-through; + } \ No newline at end of file diff --git a/src/lib/holidayUtils.ts b/src/lib/holidayUtils.ts index 35296c7..51acb95 100644 --- a/src/lib/holidayUtils.ts +++ b/src/lib/holidayUtils.ts @@ -31,49 +31,56 @@ export function getHolidaysForYear(countryCode: string, year: number, stateCode? } // Find optimal placement of PTO days to maximize consecutive time off -export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6]): Date[] { +export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6], startDate?: Date): Date[] { + const effectiveStartDate = startDate || new Date(year, 0, 1); + const filteredHolidays = holidays.filter(h => h.date.getFullYear() === year && h.date >= effectiveStartDate); const allDaysOff = new Set([ - ...holidays.filter(h => h.date.getFullYear() === year).map(h => dateKey(h.date)), - ...getWeekends(year, weekendDays).map(d => dateKey(d)) + ...filteredHolidays.map(h => dateKey(h.date)), + ...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d)) ]); - const gaps = findGaps(allDaysOff, year, weekendDays); + const gaps = findGaps(allDaysOff, year, weekendDays, effectiveStartDate); return selectDaysOff(rankGapsByEfficiency(gaps, allDaysOff, weekendDays), daysOff, allDaysOff, weekendDays); } // Calculate periods of consecutive days off (weekends + holidays + PTO) -export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6]) { +export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6], startDate?: Date) { + const effectiveStartDate = startDate || new Date(year, 0, 1); + const filteredHolidays = holidays.filter(h => h.date >= effectiveStartDate); + const filteredOptimizedDaysOff = optimizedDaysOff.filter(d => d >= effectiveStartDate); + const allDaysOff = new Set([ - ...holidays.map(h => dateKey(h.date)), - ...optimizedDaysOff.map(d => dateKey(d)), - ...getWeekends(year, weekendDays).map(d => dateKey(d)) + ...filteredHolidays.map(h => dateKey(h.date)), + ...filteredOptimizedDaysOff.map(d => dateKey(d)), + ...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d)) ]); const consecutiveDaysOff = []; let currentGroup = []; - for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { - if (isWeekend(d, weekendDays) || isHoliday(d, holidays) || allDaysOff.has(dateKey(d))) { + for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { + if (isWeekend(d, weekendDays) || isHoliday(d, filteredHolidays) || allDaysOff.has(dateKey(d))) { currentGroup.push(new Date(d)); } else if (currentGroup.length > 0) { if (isValidConsecutiveGroup(currentGroup, weekendDays)) { - consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff)); + consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff)); } currentGroup = []; } } if (currentGroup.length > 0 && isValidConsecutiveGroup(currentGroup, weekendDays)) { - consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff)); + consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff)); } return consecutiveDaysOff; } // Get all weekend days for a year -function getWeekends(year: number, weekendDays: number[]): Date[] { +function getWeekends(year: number, weekendDays: number[], startDate?: Date): Date[] { + const effectiveStartDate = startDate || new Date(year, 0, 1); const weekends = []; - for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { + for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { if (d.getMonth() === d.getMonth() && isWeekend(d, weekendDays)) { weekends.push(new Date(d)); } @@ -82,11 +89,12 @@ function getWeekends(year: number, weekendDays: number[]): Date[] { } // Find gaps between days off that could be filled with PTO -function findGaps(allDaysOff: Set, year: number, weekendDays: number[]) { +function findGaps(allDaysOff: Set, year: number, weekendDays: number[], startDate?: Date) { + const effectiveStartDate = startDate || new Date(year, 0, 1); const gaps = []; let gapStart = null; - for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { + for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { if (!allDaysOff.has(dateKey(d)) && !isWeekend(d, weekendDays)) { if (!gapStart) gapStart = new Date(d); } else if (gapStart) { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5fa057c..2f109d3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -17,6 +17,8 @@ let daysOff: number = 0; let optimizedDaysOff: Date[] = []; let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }> = []; + let showExcludedMonths: boolean = true; + let visibleMonths: number[] = []; let countriesInput: HTMLInputElement | null = null; let statesInput: HTMLInputElement | null = null; let showHowItWorks: boolean = false; @@ -35,12 +37,27 @@ let showWeekendSettings: boolean = false; let weekendDays: number[] = []; + // Start date state + let startDate: Date = new Date(new Date().getFullYear(), 0, 1); + let showDatePicker: boolean = false; + let datePickerValue: string = ''; + $: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || ''; - $: if (selectedCountryCode || selectedStateCode || daysOff || year) { + $: if (selectedCountryCode || selectedStateCode || daysOff || year || startDate) { updateHolidays(); } + // Reactive: when year changes, load start date for that year + $: if (year !== undefined && year) { + startDate = getStartDate(year); + } + + // Reactive: when startDate or year changes, update excluded months visibility + $: if (year !== undefined && year && startDate) { + showExcludedMonths = !hasExcludedMonths(); + } + $: if (daysOff) { localStorage.setItem('daysOff', daysOff.toString()); } @@ -59,8 +76,11 @@ const stateName = target.value; selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName) || ''; selectedState = stateName; - localStorage.setItem('selectedState', selectedState); - localStorage.setItem('selectedStateCode', selectedStateCode); + // Save state per country + if (selectedCountryCode) { + localStorage.setItem(`selectedState_${selectedCountryCode}`, selectedState); + localStorage.setItem(`selectedStateCode_${selectedCountryCode}`, selectedStateCode); + } } onMount(() => { @@ -73,14 +93,22 @@ const storedYear = localStorage.getItem('year'); const storedCountry = localStorage.getItem('selectedCountry'); const storedDaysOff = localStorage.getItem('daysOff'); - const storedState = localStorage.getItem('selectedState'); - const storedStateCode = localStorage.getItem('selectedStateCode'); year = storedYear ? parseInt(storedYear, 10) : defaultYear; selectedCountry = storedCountry || defaultCountry; daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff; - selectedState = storedState || ''; - selectedStateCode = storedStateCode || ''; + + // Load state per country + const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || ''; + if (countryCode) { + selectedState = localStorage.getItem(`selectedState_${countryCode}`) || ''; + selectedStateCode = localStorage.getItem(`selectedStateCode_${countryCode}`) || ''; + } else { + selectedState = ''; + selectedStateCode = ''; + } + startDate = getStartDate(year); + // showExcludedMonths will be set by reactive statement updateHolidays(); }); @@ -113,14 +141,17 @@ function handleCountryChange(event: Event) { const target = event.target as HTMLInputElement; const fullValue = target.value; - if (selectedCountryCode) { - daysOff = ptoData[selectedCountryCode] || 0; - selectedState = ''; // Reset state - selectedStateCode = ''; // Reset state code - updateStatesList(selectedCountryCode); // Update states list for the new country + selectedCountry = fullValue; + // Get the country code for the new country (selectedCountryCode will update reactively) + const newCountryCode = Object.keys(countriesList).find(code => countriesList[code] === fullValue) || ''; + if (newCountryCode) { + // Update days off to the new country's default + daysOff = ptoData[newCountryCode] || 0; + // Load state for the new country + selectedState = localStorage.getItem(`selectedState_${newCountryCode}`) || ''; + selectedStateCode = localStorage.getItem(`selectedStateCode_${newCountryCode}`) || ''; + // updateStatesList and updateHolidays will be called by reactive statements localStorage.setItem('selectedCountry', selectedCountry); - localStorage.setItem('selectedState', selectedState); - localStorage.setItem('selectedStateCode', selectedStateCode); localStorage.setItem('daysOff', daysOff.toString()); } } @@ -135,8 +166,8 @@ hidden: isHolidayHidden(holiday) })); const visibleHolidays = holidays.filter(h => !h.hidden); - optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays); - consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays); + optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays, startDate); + consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays, startDate); } else { holidays = []; optimizedDaysOff = []; @@ -147,14 +178,18 @@ function resetToDefault() { year = defaultYear; selectedCountry = defaultCountry; - selectedState = ''; - selectedStateCode = ''; - daysOff = defaultDaysOff; + const defaultCountryCode = Object.keys(countriesList).find(code => countriesList[code] === defaultCountry) || ''; + // Load state for default country + if (defaultCountryCode) { + selectedState = localStorage.getItem(`selectedState_${defaultCountryCode}`) || ''; + selectedStateCode = localStorage.getItem(`selectedStateCode_${defaultCountryCode}`) || ''; + } else { + selectedState = ''; + selectedStateCode = ''; + } + // Keep current daysOff value, don't reset it localStorage.setItem('year', year.toString()); localStorage.setItem('selectedCountry', selectedCountry); - localStorage.setItem('selectedState', selectedState); - localStorage.setItem('selectedStateCode', selectedStateCode); - localStorage.setItem('daysOff', daysOff.toString()); } function handleKeyDown(event: KeyboardEvent) { @@ -252,6 +287,156 @@ $: visibleHolidaysCount = holidays.filter(h => !h.hidden).length; + // Get start date for a given year from localStorage + function getStartDate(year: number): Date { + try { + const stored = localStorage.getItem('startDates'); + if (stored) { + const startDates: string[] = JSON.parse(stored); + // Find date that matches the year (extract year from date string) + const dateStr = startDates.find(date => { + const dateYear = parseInt(date.split('-')[0] || date.split('T')[0].split('-')[0]); + return dateYear === year; + }); + if (dateStr) { + // Parse date string - handle both YYYY-MM-DD and ISO format + const parsed = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr; + const [y, m, d] = parsed.split('-').map(Number); + return new Date(y, m - 1, d); + } + } + } catch (e) { + console.error('Error loading start date:', e); + } + return new Date(year, 0, 1); // Default to Jan 1st + } + + // Save start date for a given year to localStorage + function saveStartDate(year: number, date: Date) { + try { + const stored = localStorage.getItem('startDates'); + let startDates: string[] = stored ? JSON.parse(stored) : []; + // Remove existing date for this year (extract year from date string) + startDates = startDates.filter(dateStr => { + const dateYear = parseInt(dateStr.split('-')[0] || dateStr.split('T')[0].split('-')[0]); + return dateYear !== year; + }); + // Add new date + startDates.push(formatDateForInput(date)); + localStorage.setItem('startDates', JSON.stringify(startDates)); + } catch (e) { + console.error('Error saving start date:', e); + } + } + + // Format date as YYYY-MM-DD for date input (no timezone conversion) + function formatDateForInput(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + // Format start date for display + function formatStartDate(date: Date): string { + const today = new Date(); + if (date.getTime() === new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime() && date.getFullYear() === year) { + return 'Today'; + } + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const day = date.getDate(); + const suffix = getDaySuffix(day); + return `${monthNames[date.getMonth()]} ${day}${suffix}`; + } + + // Get day suffix (st, nd, rd, th) + function getDaySuffix(day: number): string { + if (day > 3 && day < 21) return 'th'; + switch (day % 10) { + case 1: return 'st'; + case 2: return 'nd'; + case 3: return 'rd'; + default: return 'th'; + } + } + + // Handle start date change + function handleStartDateChange(newDate: Date) { + startDate = newDate; + saveStartDate(year, newDate); + updateHolidays(); + // showExcludedMonths will be updated by reactive statement + } + + // Handle date picker input change (auto-save) + function handleDatePickerChange() { + if (datePickerValue) { + // Parse YYYY-MM-DD format in local time (consistent with getStartDate) + const [y, m, d] = datePickerValue.split('-').map(Number); + const newDate = new Date(y, m - 1, d); + handleStartDateChange(newDate); + } + } + + // Set start date to today + function setStartDateToToday() { + const today = new Date(); + if (today.getFullYear() === year) { + handleStartDateChange(new Date(today.getFullYear(), today.getMonth(), today.getDate())); + showDatePicker = false; + } + } + + // Reset start date to Jan 1st + function resetStartDateToJan1() { + const jan1st = new Date(year, 0, 1); + handleStartDateChange(jan1st); + showDatePicker = false; + } + + // Check if today is in the current year + function isTodayInYear(): boolean { + const today = new Date(); + return today.getFullYear() === year; + } + + // Helper: Get start date normalized to the current year + function getStartDateInYear(): Date { + return new Date(year, startDate.getMonth(), startDate.getDate()); + } + + // Check if a month is active (not entirely before the start date) + function isMonthActive(monthIndex: number): boolean { + const startDateInYear = getStartDateInYear(); + const startMonth = startDateInYear.getMonth(); + + // Month is active if: + // 1. The start date falls within this month (same month), OR + // 2. The month starts on or after the start date (later month) + // This means only months entirely before the start date's month are excluded + return monthIndex >= startMonth; + } + + // Check if start date is Jan 1st + function isStartDateJan1st(): boolean { + const startDateInYear = getStartDateInYear(); + return startDateInYear.getTime() === new Date(year, 0, 1).getTime(); + } + + // Check if there are any excluded months (months entirely before the start date) + function hasExcludedMonths(): boolean { + return months.some(month => !isMonthActive(month)); + } + + // Filter months based on showExcludedMonths setting + // Explicitly depend on startDate, year, and showExcludedMonths to ensure proper reactivity + // Use a computed value that depends on all relevant variables + $: visibleMonths = (startDate && year !== undefined && year) + ? (showExcludedMonths + ? months + : months.filter(month => isMonthActive(month))) + : []; + function toggleWeekendDay(dayNumber: number) { if (weekendDays.includes(dayNumber)) { weekendDays = weekendDays.filter(d => d !== dayNumber); @@ -318,7 +503,7 @@ .content-box p { text-align: center; - line-height: 2; + line-height: 3; } input { @@ -365,6 +550,29 @@ } } + .toggle-excluded-months-container { + text-align: center; + margin: 20px 0; + } + + .toggle-excluded-months { + padding: 8px 16px; + background-color: #333; + border: 1px solid #555; + border-radius: 5px; + color: #fff; + cursor: pointer; + font-size: 0.9em; + transition: background-color 0.3s; + width: auto; + display: inline-block; + white-space: nowrap; + } + + .toggle-excluded-months:hover { + background-color: #444; + } + .calendar-container { width: 100%; max-width: 300px; @@ -557,7 +765,6 @@ } .content-box button { - margin-left: 10px; background-color: #444; border: none; color: #fff; @@ -641,6 +848,161 @@ margin-bottom: 15px; color: #fff; } + + .start-date-link { + text-decoration: underline; + text-decoration-style: dotted; + cursor: pointer; + color: inherit; + } + + .start-date-link:hover { + text-decoration-style: solid; + } + + .date-picker-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .date-picker-modal { + background-color: #222; + border-radius: 10px; + padding: 25px; + max-width: min(400px, calc(100vw - 40px)); + width: 100%; + box-sizing: border-box; + position: relative; + color: #fff; + } + + .date-picker-close { + position: absolute; + top: 10px; + right: 10px; + background: transparent; + border: none; + color: #fff; + font-size: 24px; + cursor: pointer; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; + } + + .date-picker-close:hover, + .date-picker-close:active, + .date-picker-close:focus { + background: transparent; + color: #ccc; + } + + .date-picker-modal h3 { + margin: 0 0 10px 0; + padding: 0; + text-align: center; + font-size: 1.5em; + } + + .date-picker-modal p { + margin: 0 0 20px 0; + padding: 0; + text-align: center; + color: #ccc; + } + + .date-picker-controls { + margin: 0 0 20px 0; + padding: 0; + width: 100%; + } + + .date-input { + width: 100%; + padding: 10px; + font-size: 1em; + background-color: #222; + border: 1px solid #555; + border-radius: 5px; + color: #fff; + box-sizing: border-box; + text-align: center; + margin: 0; + display: block; + } + + .date-input:focus { + outline: 2px solid #61dafb; + border-color: #61dafb; + } + + /* Make calendar icon white */ + .date-input::-webkit-calendar-picker-indicator { + filter: invert(1); + cursor: pointer; + } + + .date-input::-webkit-inner-spin-button, + .date-input::-webkit-clear-button { + filter: invert(1); + } + + .date-picker-buttons { + display: flex; + gap: 10px; + justify-content: center; + margin: 0 0 15px 0; + width: 100%; + box-sizing: border-box; + } + + .date-picker-button, + .date-picker-save { + margin: 0; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + box-sizing: border-box; + } + + .date-picker-button { + flex: 1; + min-width: 0; + padding: 20px; + background-color: #444; + color: #fff; + font-size: 0.9em; + } + + .date-picker-button:hover { + background-color: #555; + } + + .date-picker-save { + width: 100%; + margin: 0; + font-size: 1.1em; + font-weight: bold; + background-color: #61dafb; + color: #000; + } + + .date-picker-save:hover { + background-color: #4fa8c5; + }
@@ -689,15 +1051,27 @@ {daysOff} - days off in + days off from + { showDatePicker = true; datePickerValue = formatDateForInput(startDate); }} class="bold start-date-link"> + {@html formatStartDate(startDate)} + + until the end of {year}

- {#if year !== defaultYear || selectedCountry !== defaultCountry || daysOff !== defaultDaysOff} - Reset to my country + {#if year !== defaultYear || selectedCountry !== defaultCountry} + {@const yearDifferent = year !== defaultYear} + {@const countryDifferent = selectedCountry !== defaultCountry} + + {yearDifferent && countryDifferent + ? 'Reset to current country and year' + : yearDifferent + ? 'Reset to current year' + : 'Reset to current country'} + {/if} @@ -781,13 +1155,27 @@
{/if} + {#if hasExcludedMonths()} +
+ +
+ {/if} +
- {#each months as month} + {#each visibleMonths as month}
+ {#if showDatePicker} +
showDatePicker = false}> +
+ +

Set Start Date

+

Choose when your time off period begins for {year}

+
+ +
+
+ {#if isTodayInYear()} + + {/if} + {#if !isStartDateJan1st()} + + {/if} +
+ +
+
+ {/if} + From 2b40c2cc880f391efc2223bbe452354f6681ff76 Mon Sep 17 00:00:00 2001 From: zachd Date: Mon, 10 Nov 2025 00:36:51 +0100 Subject: [PATCH 2/2] Add tests --- package-lock.json | 1236 +++++++++++++++++++++++++++++++--- package.json | 9 +- src/lib/holidayUtils.test.ts | 781 +++++++++++++++++++++ vite.config.ts | 7 +- 4 files changed, 1922 insertions(+), 111 deletions(-) create mode 100644 src/lib/holidayUtils.test.ts diff --git a/package-lock.json b/package-lock.json index 4fedc79..e7a2f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,12 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@vitest/ui": "^4.0.8", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", - "vite": "^5.0.3" + "vite": "^5.0.3", + "vitest": "^4.0.8" } }, "node_modules/@ampproject/remapping": { @@ -324,6 +326,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -341,6 +360,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -358,6 +394,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -462,9 +515,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, @@ -487,9 +540,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz", - "integrity": "sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz", + "integrity": "sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==", "cpu": [ "arm" ], @@ -501,9 +554,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz", - "integrity": "sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.1.tgz", + "integrity": "sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==", "cpu": [ "arm64" ], @@ -515,9 +568,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz", - "integrity": "sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.1.tgz", + "integrity": "sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==", "cpu": [ "arm64" ], @@ -529,9 +582,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz", - "integrity": "sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.1.tgz", + "integrity": "sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==", "cpu": [ "x64" ], @@ -543,9 +596,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz", - "integrity": "sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.1.tgz", + "integrity": "sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==", "cpu": [ "arm64" ], @@ -557,9 +610,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz", - "integrity": "sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.1.tgz", + "integrity": "sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==", "cpu": [ "x64" ], @@ -571,9 +624,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz", - "integrity": "sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.1.tgz", + "integrity": "sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==", "cpu": [ "arm" ], @@ -585,9 +638,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz", - "integrity": "sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.1.tgz", + "integrity": "sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==", "cpu": [ "arm" ], @@ -599,9 +652,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz", - "integrity": "sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.1.tgz", + "integrity": "sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==", "cpu": [ "arm64" ], @@ -613,9 +666,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz", - "integrity": "sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.1.tgz", + "integrity": "sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==", "cpu": [ "arm64" ], @@ -626,10 +679,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz", - "integrity": "sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.1.tgz", + "integrity": "sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.1.tgz", + "integrity": "sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==", "cpu": [ "ppc64" ], @@ -641,9 +708,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz", - "integrity": "sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.1.tgz", + "integrity": "sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.1.tgz", + "integrity": "sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==", "cpu": [ "riscv64" ], @@ -655,9 +736,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz", - "integrity": "sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.1.tgz", + "integrity": "sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==", "cpu": [ "s390x" ], @@ -669,9 +750,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz", - "integrity": "sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.1.tgz", + "integrity": "sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==", "cpu": [ "x64" ], @@ -683,9 +764,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz", - "integrity": "sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.1.tgz", + "integrity": "sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==", "cpu": [ "x64" ], @@ -696,10 +777,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.1.tgz", + "integrity": "sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz", - "integrity": "sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.1.tgz", + "integrity": "sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==", "cpu": [ "arm64" ], @@ -711,9 +806,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz", - "integrity": "sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.1.tgz", + "integrity": "sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==", "cpu": [ "ia32" ], @@ -724,10 +819,10 @@ "win32" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz", - "integrity": "sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.1.tgz", + "integrity": "sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==", "cpu": [ "x64" ], @@ -738,6 +833,27 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz", + "integrity": "sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@sveltejs/adapter-auto": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", @@ -824,6 +940,17 @@ "vite": "^5.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -831,13 +958,126 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", + "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.8", + "@vitest/utils": "4.0.8", + "chai": "^6.2.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", + "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", + "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", + "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", + "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.8.tgz", + "integrity": "sha512-F9jI5rSstNknPlTlPN2gcc4gpbaagowuRzw/OJzl368dvPun668Q182S8Q8P9PITgGCl5LAKXpzuue106eM4wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.8", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.8" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", + "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.8", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -877,6 +1117,16 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astronomia": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.1.1.tgz", @@ -908,6 +1158,16 @@ "node": ">=12.0.0" } }, + "node_modules/chai": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -1002,9 +1262,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1041,6 +1301,13 @@ "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==", "license": "MIT" }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1098,12 +1365,35 @@ "@types/estree": "^1.0.1" } }, - "node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -1113,6 +1403,20 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1217,13 +1521,13 @@ "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/moment": { @@ -1275,9 +1579,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -1293,6 +1597,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1300,10 +1611,23 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1321,8 +1645,8 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -1353,13 +1677,13 @@ } }, "node_modules/rollup": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.25.0.tgz", - "integrity": "sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.1.tgz", + "integrity": "sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -1369,24 +1693,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.25.0", - "@rollup/rollup-android-arm64": "4.25.0", - "@rollup/rollup-darwin-arm64": "4.25.0", - "@rollup/rollup-darwin-x64": "4.25.0", - "@rollup/rollup-freebsd-arm64": "4.25.0", - "@rollup/rollup-freebsd-x64": "4.25.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.25.0", - "@rollup/rollup-linux-arm-musleabihf": "4.25.0", - "@rollup/rollup-linux-arm64-gnu": "4.25.0", - "@rollup/rollup-linux-arm64-musl": "4.25.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.25.0", - "@rollup/rollup-linux-riscv64-gnu": "4.25.0", - "@rollup/rollup-linux-s390x-gnu": "4.25.0", - "@rollup/rollup-linux-x64-gnu": "4.25.0", - "@rollup/rollup-linux-x64-musl": "4.25.0", - "@rollup/rollup-win32-arm64-msvc": "4.25.0", - "@rollup/rollup-win32-ia32-msvc": "4.25.0", - "@rollup/rollup-win32-x64-msvc": "4.25.0", + "@rollup/rollup-android-arm-eabi": "4.53.1", + "@rollup/rollup-android-arm64": "4.53.1", + "@rollup/rollup-darwin-arm64": "4.53.1", + "@rollup/rollup-darwin-x64": "4.53.1", + "@rollup/rollup-freebsd-arm64": "4.53.1", + "@rollup/rollup-freebsd-x64": "4.53.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.1", + "@rollup/rollup-linux-arm-musleabihf": "4.53.1", + "@rollup/rollup-linux-arm64-gnu": "4.53.1", + "@rollup/rollup-linux-arm64-musl": "4.53.1", + "@rollup/rollup-linux-loong64-gnu": "4.53.1", + "@rollup/rollup-linux-ppc64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-musl": "4.53.1", + "@rollup/rollup-linux-s390x-gnu": "4.53.1", + "@rollup/rollup-linux-x64-gnu": "4.53.1", + "@rollup/rollup-linux-x64-musl": "4.53.1", + "@rollup/rollup-openharmony-arm64": "4.53.1", + "@rollup/rollup-win32-arm64-msvc": "4.53.1", + "@rollup/rollup-win32-ia32-msvc": "4.53.1", + "@rollup/rollup-win32-x64-gnu": "4.53.1", + "@rollup/rollup-win32-x64-msvc": "4.53.1", "fsevents": "~2.3.2" } }, @@ -1410,10 +1738,17 @@ "dev": true, "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1435,6 +1770,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/svelte": { "version": "5.1.13", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.13.tgz", @@ -1495,6 +1844,47 @@ "globrex": "^0.1.2" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -1598,6 +1988,636 @@ } } }, + "node_modules/vitest": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", + "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.8", + "@vitest/mocker": "4.0.8", + "@vitest/pretty-format": "4.0.8", + "@vitest/runner": "4.0.8", + "@vitest/snapshot": "4.0.8", + "@vitest/spy": "4.0.8", + "@vitest/utils": "4.0.8", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.8", + "@vitest/browser-preview": "4.0.8", + "@vitest/browser-webdriverio": "4.0.8", + "@vitest/ui": "4.0.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", + "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zimmerframe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", diff --git a/package.json b/package.json index 532b413..286c316 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,21 @@ "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@vitest/ui": "^4.0.8", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", - "vite": "^5.0.3" + "vite": "^5.0.3", + "vitest": "^4.0.8" }, "dependencies": { "date-holidays": "^3.23.12", diff --git a/src/lib/holidayUtils.test.ts b/src/lib/holidayUtils.test.ts new file mode 100644 index 0000000..b114603 --- /dev/null +++ b/src/lib/holidayUtils.test.ts @@ -0,0 +1,781 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getHolidaysForYear, optimizeDaysOff, calculateConsecutiveDaysOff } from './holidayUtils'; + +// Test constants +const TEST_YEAR = 2024; +const DEFAULT_WEEKENDS = [0, 6]; // Sunday, Saturday +const CUSTOM_WEEKENDS = [5, 6]; // Friday, Saturday + +// Mock browser APIs +const mockNavigator = { + languages: ['en', 'en-US'] +}; + +const mockIntlDateTimeFormat = vi.fn(() => ({ + resolvedOptions: () => ({ timeZone: 'America/New_York' }) +})); + +describe('holidayUtils', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal('navigator', mockNavigator); + vi.stubGlobal('Intl', { + ...Intl, + DateTimeFormat: mockIntlDateTimeFormat + }); + }); + + describe('getHolidaysForYear', () => { + it('should return holidays for a given year and country', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + expect(holidays).toBeDefined(); + expect(Array.isArray(holidays)).toBe(true); + expect(holidays.length).toBeGreaterThan(0); + }); + + it('should filter only public holidays', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + holidays.forEach(holiday => { + expect(holiday).toHaveProperty('date'); + expect(holiday).toHaveProperty('name'); + expect(holiday.date).toBeInstanceOf(Date); + expect(typeof holiday.name).toBe('string'); + }); + }); + + it('should handle state codes', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR, 'CA'); + expect(holidays).toBeDefined(); + expect(Array.isArray(holidays)).toBe(true); + }); + + it('should return holidays sorted by date', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + for (let i = 1; i < holidays.length; i++) { + const prev = holidays[i - 1].date.getTime(); + const curr = holidays[i].date.getTime(); + expect(curr).toBeGreaterThanOrEqual(prev); + } + }); + + it('should handle different countries', () => { + const usHolidays = getHolidaysForYear('US', TEST_YEAR); + const gbHolidays = getHolidaysForYear('GB', TEST_YEAR); + + expect(usHolidays.length).toBeGreaterThan(0); + expect(gbHolidays.length).toBeGreaterThan(0); + expect(usHolidays.length).not.toBe(gbHolidays.length); + }); + + it('should expand multi-day holidays correctly', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + const dateKeys = new Set(holidays.map(h => + `${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}` + )); + expect(holidays.length).toBeGreaterThanOrEqual(dateKeys.size); + }); + + it('should sort holidays by date first, then by name', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + for (let i = 1; i < holidays.length; i++) { + const prev = holidays[i - 1]; + const curr = holidays[i]; + const prevTime = prev.date.getTime(); + const currTime = curr.date.getTime(); + + if (prevTime === currTime) { + expect(curr.name.localeCompare(prev.name)).toBeGreaterThanOrEqual(0); + } else { + expect(currTime).toBeGreaterThan(prevTime); + } + } + }); + }); + + describe('optimizeDaysOff', () => { + const mockHolidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 6, 4), name: 'Independence Day' }, + ]; + + describe('basic functionality', () => { + it('should return an array of dates', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + result.forEach(date => { + expect(date).toBeInstanceOf(Date); + }); + }); + + it('should return at most the requested number of days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5); + expect(result.length).toBeLessThanOrEqual(5); + }); + + it('should handle zero days off', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 0); + expect(result).toEqual([]); + }); + + it('should handle more days off than available gaps', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 1000); + expect(result.length).toBeGreaterThan(0); + expect(result.length).toBeLessThan(1000); + }); + }); + + describe('exclusion rules', () => { + it('should not include weekends in optimized days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 10); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should not include holidays in optimized days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 10); + const holidayKeys = new Set(mockHolidays.map(h => + `${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}` + )); + result.forEach(date => { + const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; + expect(holidayKeys.has(dateKey)).toBe(false); + }); + }); + + it('should not select days that are already holidays or weekends', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + const holidaySet = new Set(holidays.map(h => + `${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}` + )); + result.forEach(date => { + const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; + expect(holidaySet.has(dateKey)).toBe(false); + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + }); + + describe('parameters', () => { + it('should respect startDate parameter', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + }); + }); + + it('should handle custom weekend days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5, CUSTOM_WEEKENDS); + result.forEach(date => { + expect(CUSTOM_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should filter holidays by year and startDate', () => { + const holidays = [ + { date: new Date(2023, 11, 31), name: 'Old Year' }, + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 5, 15), name: 'Mid Year' }, + ]; + const startDate = new Date(TEST_YEAR, 2, 1); + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('gap finding and prioritization', () => { + it('should only find gaps of MAX_GAP_LENGTH (5) days or less', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Wed' }, + { date: new Date(TEST_YEAR, 0, 10), name: 'Wed' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + expect(Array.isArray(result)).toBe(true); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should find and fill gaps of 1-5 days', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Wed Holiday' }, + { date: new Date(TEST_YEAR, 0, 11), name: 'Thu Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + expect(result.length).toBeGreaterThan(0); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should prioritize gaps that create longer consecutive periods', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Thursday Holiday' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Monday Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(result.length).toBe(1); + }); + + it('should prioritize smaller gaps when they create longer chains', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu Holiday' }, + { date: new Date(TEST_YEAR, 0, 9), name: 'Tue Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(result.length).toBe(1); + expect(result[0].getDate()).toBe(5); + }); + + it('should handle multiple gaps and select most efficient ones first', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Mon' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 3); + expect(result.length).toBeLessThanOrEqual(3); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should handle backward vs forward chain calculation', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 5), name: 'Friday Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(Array.isArray(result)).toBe(true); + }); + + it('should optimize to create longer consecutive periods', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(result.length).toBeGreaterThan(0); + }); + }); + + describe('edge cases', () => { + it('should handle partial gap filling when daysOff is less than gap length', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Mon' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 2); + expect(result.length).toBe(2); + result.forEach(date => { + expect(date.getDate()).toBeGreaterThanOrEqual(2); + expect(date.getDate()).toBeLessThanOrEqual(6); + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should handle multiple gaps when daysOff exceeds single gap capacity', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Wed' }, + { date: new Date(TEST_YEAR, 0, 5), name: 'Fri' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 3); + expect(result.length).toBeGreaterThanOrEqual(2); + expect(result.length).toBeLessThanOrEqual(3); + }); + + it('should handle optimization with no available gaps', () => { + const holidays = Array.from({ length: 365 }, (_, i) => { + const date = new Date(TEST_YEAR, 0, 1); + date.setDate(date.getDate() + i); + if (date.getDay() !== 0 && date.getDay() !== 6) { + return { date, name: `Holiday ${i}` }; + } + return null; + }).filter(Boolean) as Array<{ date: Date; name: string }>; + + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(result).toEqual([]); + }); + + it('should handle gaps at the start of the year', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 5), name: 'Holiday' }, + ]; + const startDate = new Date(TEST_YEAR, 0, 1); + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle gaps at the end of the year', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + result.forEach(date => { + expect(date.getFullYear()).toBe(TEST_YEAR); + expect(date.getMonth()).toBeLessThanOrEqual(11); + }); + }); + + it('should handle gaps that span year boundaries correctly', () => { + const startDate = new Date(TEST_YEAR, 11, 20); + const holidays = [ + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getFullYear()).toBe(TEST_YEAR); + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + }); + }); + }); + }); + + describe('calculateConsecutiveDaysOff', () => { + const mockHolidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 0, 15), name: 'Holiday' }, + ]; + + describe('basic functionality', () => { + it('should return an array of periods', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + expect(Array.isArray(result)).toBe(true); + }); + + it('should calculate periods with correct structure', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + expect(period).toHaveProperty('startDate'); + expect(period).toHaveProperty('endDate'); + expect(period).toHaveProperty('totalDays'); + expect(period).toHaveProperty('usedDaysOff'); + expect(period.startDate).toBeInstanceOf(Date); + expect(period.endDate).toBeInstanceOf(Date); + expect(typeof period.totalDays).toBe('number'); + expect(typeof period.usedDaysOff).toBe('number'); + }); + }); + + it('should include holidays in consecutive periods', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + const hasPeriodWithHoliday = result.some(period => { + const holidayDate = mockHolidays[0].date; + return period.startDate <= holidayDate && period.endDate >= holidayDate; + }); + expect(hasPeriodWithHoliday).toBe(true); + }); + }); + + describe('calculations', () => { + it('should calculate totalDays correctly', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + const calculatedDays = Math.round( + (period.endDate.getTime() - period.startDate.getTime()) / (1000 * 60 * 60 * 24) + ) + 1; + expect(period.totalDays).toBe(calculatedDays); + }); + }); + + it('should count usedDaysOff correctly', () => { + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 3), + ]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + expect(period.usedDaysOff).toBeGreaterThanOrEqual(0); + const daysInPeriod = optimizedDays.filter(day => + day >= period.startDate && day <= period.endDate + ).length; + expect(period.usedDaysOff).toBeLessThanOrEqual(daysInPeriod); + }); + }); + + it('should correctly count usedDaysOff in periods', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + ]; + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 3), + ]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + const periodWithOptimized = result.find(period => + optimizedDays.some(day => day >= period.startDate && day <= period.endDate) + ); + if (periodWithOptimized) { + const daysInPeriod = optimizedDays.filter(day => + day >= periodWithOptimized.startDate && day <= periodWithOptimized.endDate + ).length; + expect(periodWithOptimized.usedDaysOff).toBe(daysInPeriod); + } + }); + }); + + describe('validation rules', () => { + it('should not include periods that are only weekends', () => { + const optimizedDays: Date[] = []; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + let allWeekends = true; + for (let d = new Date(period.startDate); d <= period.endDate; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + if (dayOfWeek !== 0 && dayOfWeek !== 6) { + allWeekends = false; + break; + } + } + expect(allWeekends).toBe(false); + }); + }); + + it('should exclude single-day periods', () => { + const holidays: Array<{ date: Date; name: string }> = []; + const optimizedDays: Date[] = []; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + }); + }); + + it('should handle groups that are only weekends correctly', () => { + const holidays: Array<{ date: Date; name: string }> = []; + const optimizedDays: Date[] = []; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + let hasNonWeekend = false; + for (let d = new Date(period.startDate); d <= period.endDate; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + if (dayOfWeek !== 0 && dayOfWeek !== 6) { + hasNonWeekend = true; + break; + } + } + expect(hasNonWeekend).toBe(true); + }); + }); + }); + + describe('parameters', () => { + it('should respect startDate parameter', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const optimizedDays = [new Date(TEST_YEAR, 5, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR, DEFAULT_WEEKENDS, startDate); + result.forEach(period => { + expect(period.startDate.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + }); + }); + + it('should handle custom weekend days', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR, CUSTOM_WEEKENDS); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle empty optimized days', () => { + const result = calculateConsecutiveDaysOff(mockHolidays, [], TEST_YEAR); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('edge cases', () => { + it('should handle periods spanning multiple months', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 31), name: 'End of Jan' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 1, 1)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle periods that start exactly at startDate', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const holidays = [ + { date: new Date(TEST_YEAR, 5, 1), name: 'Start Holiday' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 5, 3)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR, DEFAULT_WEEKENDS, startDate); + if (result.length > 0) { + expect(result[0].startDate.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + } + }); + + it('should handle periods that end exactly at year end (Dec 31)', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 11, 30), name: 'Dec 30' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 11, 31)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + const periodAtYearEnd = result.find(period => + period.endDate.getMonth() === 11 && period.endDate.getDate() === 31 + ); + if (periodAtYearEnd) { + expect(periodAtYearEnd.endDate.getFullYear()).toBe(TEST_YEAR); + } + }); + + it('should correctly handle overlapping optimized days and holidays', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + ]; + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 3), + ]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + const period = result.find(p => + p.startDate <= holidays[0].date && p.endDate >= optimizedDays[1] + ); + if (period) { + expect(period.usedDaysOff).toBe(2); + } + }); + + it('should handle consecutive periods separated by work days', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu' }, + ]; + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 5), + ]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + expect(result.length).toBeGreaterThanOrEqual(1); + result.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + }); + }); + }); + }); + + describe('Integration tests', () => { + it('should work together: get holidays, optimize, and calculate periods', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 10); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + expect(holidays.length).toBeGreaterThan(0); + expect(optimizedDays.length).toBeLessThanOrEqual(10); + expect(Array.isArray(periods)).toBe(true); + + periods.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + expect(period.usedDaysOff).toBeGreaterThanOrEqual(0); + expect(period.startDate <= period.endDate).toBe(true); + }); + }); + + it('should optimize efficiently to maximize consecutive days', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Holiday' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 1); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + if (periods.length > 0) { + const hasOptimizedDay = periods.some(period => + optimizedDays.some(day => + day >= period.startDate && day <= period.endDate + ) + ); + expect(hasOptimizedDay).toBe(true); + } + }); + + it('should handle edge case: all days are holidays or weekends', () => { + const holidays = Array.from({ length: 50 }, (_, i) => ({ + date: new Date(TEST_YEAR, 0, i + 1), + name: `Holiday ${i + 1}` + })); + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 5); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + expect(Array.isArray(optimizedDays)).toBe(true); + expect(Array.isArray(periods)).toBe(true); + }); + }); + + describe('Edge cases and error handling', () => { + it('should handle year with no holidays gracefully', () => { + const result = optimizeDaysOff([], TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle invalid country codes gracefully', () => { + try { + const holidays = getHolidaysForYear('XX', TEST_YEAR); + expect(Array.isArray(holidays)).toBe(true); + } catch (e) { + expect(e).toBeDefined(); + } + }); + + it('should handle dates at year boundaries', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 11, 31), name: 'New Year Eve' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle startDate at end of year', () => { + const startDate = new Date(TEST_YEAR, 11, 15); + const holidays = [ + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + expect(date.getFullYear()).toBe(TEST_YEAR); + }); + }); + + it('should handle holidays from previous year correctly', () => { + const holidays = [ + { date: new Date(2023, 11, 31), name: 'Old Year' }, + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle leap year correctly', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 1, 29), name: 'Leap Day' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('Private function behavior (tested indirectly)', () => { + it('should correctly identify weekend days', () => { + const holidays: Array<{ date: Date; name: string }> = []; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should correctly calculate days between dates', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Start' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 0, 5)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + const calculated = Math.round( + (period.endDate.getTime() - period.startDate.getTime()) / (1000 * 60 * 60 * 24) + ) + 1; + expect(period.totalDays).toBe(calculated); + }); + }); + + it('should generate consistent date keys', () => { + const date1 = new Date(TEST_YEAR, 0, 15); + const date2 = new Date(TEST_YEAR, 0, 15); + const holidays = [ + { date: date1, name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + const hasDate2 = result.some(d => + d.getFullYear() === date2.getFullYear() && + d.getMonth() === date2.getMonth() && + d.getDate() === date2.getDate() + ); + expect(hasDate2).toBe(false); + }); + + it('should correctly identify holidays using dateKey', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 15, 10, 30), name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + const hasHolidayDate = result.some(d => + d.getFullYear() === TEST_YEAR && + d.getMonth() === 0 && + d.getDate() === 15 + ); + expect(hasHolidayDate).toBe(false); + }); + + it('should correctly get weekends for the year with startDate', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const holidays: Array<{ date: Date; name: string }> = []; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + }); + + describe('Complex scenarios and real-world cases', () => { + it('should handle a typical year with multiple holidays and weekends', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 4, 27), name: 'Memorial Day' }, + { date: new Date(TEST_YEAR, 6, 4), name: 'Independence Day' }, + { date: new Date(TEST_YEAR, 8, 2), name: 'Labor Day' }, + { date: new Date(TEST_YEAR, 10, 28), name: 'Thanksgiving' }, + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 10); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + expect(optimizedDays.length).toBeLessThanOrEqual(10); + expect(periods.length).toBeGreaterThan(0); + + periods.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + expect(period.startDate <= period.endDate).toBe(true); + expect(period.usedDaysOff).toBeGreaterThanOrEqual(0); + }); + }); + + it('should maximize consecutive days off efficiently', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Mon' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 1); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + const periodWithOptimized = periods.find(p => + optimizedDays.some(day => day >= p.startDate && day <= p.endDate) + ); + expect(periodWithOptimized).toBeDefined(); + if (periodWithOptimized) { + expect(periodWithOptimized.totalDays).toBeGreaterThanOrEqual(4); + } + }); + + it('should handle non-standard weekend configurations', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 5, CUSTOM_WEEKENDS); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR, CUSTOM_WEEKENDS); + + optimizedDays.forEach(date => { + expect(CUSTOM_WEEKENDS).not.toContain(date.getDay()); + }); + + expect(Array.isArray(periods)).toBe(true); + }); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index bbf8c7d..b233f82 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,5 +2,10 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit()], + test: { + globals: true, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'] + } });