weather_app/tests/lib/api/moon.test.js
hermes-explorigin 97b676093a
All checks were successful
Test, Build & Deploy / test-and-build (push) Successful in 23s
Test, Build & Deploy / deploy (push) Successful in 21s
Add moon phase card to Weather Details
- New src/lib/api/moon.js: fetches daily moon_phase from Open-Meteo
  (independent of the NWS/Open-Meteo weather source, since NWS has no moon
  data), plus pure helpers: moonName (8 phases), illumination fraction, and
  nextMoonPhase, which unwraps the ~29.5-day cycle and interpolates the next
  quarter boundary (new/first quarter/full/last quarter), returning its name,
  date, and days until it.
- New MoonPhaseCard.svelte: renders an SVG moon (two-circle terminator that
  correctly shows crescent/quarter/gibbous shapes), the current phase name,
  percent illuminated, and the next phase with date. Caches per-location via
  an effect; degrades gracefully when unavailable.
- Wired into WeatherDetail between Sun and UV and Temperature Range.
- 11 unit tests for moonName/illumination/interpolation/nextMoonPhase.
- Verified in headless Chromium: card renders, moon shape correct, no errors.
2026-09-08 02:25:01 +00:00

85 lines
2.9 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import { moonName, illumination, nextMoonPhase, interpolateDate, diffDays } from '../../../src/lib/api/moon.js'
describe('moonName', () => {
it('names the four principal phases', () => {
expect(moonName(0.0)).toBe('New Moon')
expect(moonName(0.25)).toBe('First Quarter')
expect(moonName(0.5)).toBe('Full Moon')
expect(moonName(0.75)).toBe('Last Quarter')
})
it('names the intermediate phases', () => {
expect(moonName(0.125)).toBe('Waxing Crescent')
expect(moonName(0.375)).toBe('Waxing Gibbous')
expect(moonName(0.625)).toBe('Waning Gibbous')
expect(moonName(0.875)).toBe('Waning Crescent')
})
it('wraps 1.0 back to new moon', () => {
expect(moonName(1.0)).toBe('New Moon')
expect(moonName(0.98)).toBe('Waning Crescent')
})
})
describe('illumination', () => {
it('is 0 at new moon and 1 at full', () => {
expect(illumination(0)).toBeCloseTo(0, 5)
expect(illumination(0.5)).toBeCloseTo(1, 5)
})
it('is symmetric about full: quarter both ~0.5', () => {
expect(illumination(0.25)).toBeCloseTo(0.5, 5)
expect(illumination(0.75)).toBeCloseTo(0.5, 5)
})
it('near new gives small illumination', () => {
expect(illumination(0.912)).toBeLessThan(0.1)
})
})
describe('interpolateDate / diffDays', () => {
it('interpolates dates', () => {
expect(interpolateDate('2026-09-10', '2026-09-12', 0.5)).toBe('2026-09-11')
expect(interpolateDate('2026-09-10', '2026-09-12', 0)).toBe('2026-09-10')
expect(interpolateDate('2026-09-10', '2026-09-12', 1)).toBe('2026-09-12')
})
it('computes whole days between dates', () => {
expect(diffDays('2026-09-08', '2026-09-10')).toBe(2)
expect(diffDays('2026-09-08', '2026-09-08')).toBe(0)
})
})
describe('nextMoonPhase', () => {
const mk = (arr) => arr.map(([date, phase]) => ({ date, phase }))
it('finds next new moon from a waning-crescent sample', () => {
const days = mk([
['2026-09-08', 0.912], ['2026-09-09', 0.948], ['2026-09-10', 0.985],
['2026-09-11', 0.020], ['2026-09-12', 0.054], ['2026-09-13', 0.088],
['2026-09-14', 0.121], ['2026-09-15', 0.153],
])
const r = nextMoonPhase(days)
expect(r.name).toBe('New Moon')
expect(r.daysUntil).toBe(2)
expect(r.date).toBe('2026-09-10')
})
it('finds next first quarter from a new-moon sample', () => {
const days = mk([
['2026-09-11', 0.020], ['2026-09-12', 0.054], ['2026-09-13', 0.088],
['2026-09-14', 0.121], ['2026-09-15', 0.153], ['2026-09-16', 0.184],
['2026-09-17', 0.215], ['2026-09-18', 0.245], ['2026-09-19', 0.276],
])
const r = nextMoonPhase(days)
expect(r.name).toBe('First Quarter')
expect(r.daysUntil).toBe(7)
})
it('returns null with too few samples', () => {
expect(nextMoonPhase(null)).toBeNull()
expect(nextMoonPhase(mk([['2026-09-08', 0.5]]))).toBeNull()
})
})