ILZHVADNALVFUHKGHS7RJ4NYV7TML4DQOJVCFJOVKK7SAAT3SODAC maybeSolarNight: astronomicalData?.maybeSolarNight || null,maybeCivilNight: astronomicalData?.maybeCivilNight || null,moonPhase: astronomicalData?.moonPhase || null,
maybeSolarNight: astronomicalData?.maybeSolarNight ?? null,maybeCivilNight: astronomicalData?.maybeCivilNight ?? null,moonPhase: astronomicalData?.moonPhase ?? null,
/*** Tests for file import API boolean field handling* Specifically tests the fix for false values being converted to null*/import { describe, it, expect } from 'vitest';describe('File Import Boolean Logic', () => {describe('Backend boolean field handling', () => {it('should demonstrate the boolean logic fix for file import data', () => {// Simulate the file import data that would be sent to the backendconst fileImportData = {fileName: 'test.wav',xxh64Hash: 'abcd1234',locationId: 'test-location',timestampLocal: '2024-06-15T12:00:00.000Z',clusterId: 'test-cluster',duration: 60,sampleRate: 44100,upload: false,maybeSolarNight: false, // This should NOT become nullmaybeCivilNight: false, // This should NOT become nullmoonPhase: 0.25};// Test the OLD buggy logic (what was happening before the fix)const buggyBackendLogic = {maybeSolarNight: fileImportData.maybeSolarNight || null,maybeCivilNight: fileImportData.maybeCivilNight || null,moonPhase: fileImportData.moonPhase?.toString() || null};// Test the FIXED logic (what should happen now)const fixedBackendLogic = {maybeSolarNight: fileImportData.maybeSolarNight ?? null,maybeCivilNight: fileImportData.maybeCivilNight ?? null,moonPhase: fileImportData.moonPhase?.toString() || null};// Demonstrate the bugexpect(buggyBackendLogic.maybeSolarNight).toBe(null); // BUG: false became nullexpect(buggyBackendLogic.maybeCivilNight).toBe(null); // BUG: false became nullexpect(buggyBackendLogic.moonPhase).toBe('0.25'); // OK: number converted to string// Demonstrate the fixexpect(fixedBackendLogic.maybeSolarNight).toBe(false); // FIXED: false stays falseexpect(fixedBackendLogic.maybeCivilNight).toBe(false); // FIXED: false stays falseexpect(fixedBackendLogic.moonPhase).toBe('0.25'); // OK: number converted to string});it('should handle all boolean combinations correctly in backend logic', () => {const testCases = [// Test case: daytime recording (false values){input: { maybeSolarNight: false, maybeCivilNight: false, moonPhase: 0.25 },expected: { maybeSolarNight: false, maybeCivilNight: false, moonPhase: '0.25' }},// Test case: nighttime recording (true values){input: { maybeSolarNight: true, maybeCivilNight: true, moonPhase: 0.75 },expected: { maybeSolarNight: true, maybeCivilNight: true, moonPhase: '0.75' }},// Test case: mixed values{input: { maybeSolarNight: true, maybeCivilNight: false, moonPhase: 0.5 },expected: { maybeSolarNight: true, maybeCivilNight: false, moonPhase: '0.5' }},// Test case: no astronomical data (undefined values){input: { maybeSolarNight: undefined, maybeCivilNight: undefined, moonPhase: undefined },expected: { maybeSolarNight: null, maybeCivilNight: null, moonPhase: null }},// Test case: explicit null values{input: { maybeSolarNight: null, maybeCivilNight: null, moonPhase: null },expected: { maybeSolarNight: null, maybeCivilNight: null, moonPhase: null }}];testCases.forEach(({ input, expected }) => {// Apply the fixed backend logicconst result = {maybeSolarNight: input.maybeSolarNight ?? null,maybeCivilNight: input.maybeCivilNight ?? null,moonPhase: input.moonPhase?.toString() || null};expect(result.maybeSolarNight).toBe(expected.maybeSolarNight);expect(result.maybeCivilNight).toBe(expected.maybeCivilNight);expect(result.moonPhase).toBe(expected.moonPhase);});});it('should validate database field types match expected values', () => {// Test that the values we're producing match what the database expectsconst databaseInsertData = {// Required fieldsfileName: 'test.wav',xxh64Hash: 'abcd1234',locationId: 'test-location',timestampLocal: new Date('2024-06-15T12:00:00.000Z'),duration: '60',sampleRate: 44100,// Optional boolean fields (the focus of our fix)maybeSolarNight: false, // Should be boolean false, not nullmaybeCivilNight: false, // Should be boolean false, not null// Other optional fieldsclusterId: 'test-cluster',description: null,upload: false,moonPhase: '0.25',// Audit fieldscreatedBy: 'test-user',createdAt: new Date(),lastModified: new Date(),modifiedBy: 'test-user',active: true};// Validate the types match the database schema expectationsexpect(typeof databaseInsertData.maybeSolarNight).toBe('boolean');expect(typeof databaseInsertData.maybeCivilNight).toBe('boolean');expect(databaseInsertData.maybeSolarNight).toBe(false);expect(databaseInsertData.maybeCivilNight).toBe(false);// These should not be null when we have valid boolean valuesexpect(databaseInsertData.maybeSolarNight).not.toBe(null);expect(databaseInsertData.maybeCivilNight).not.toBe(null);});});describe('Nullish coalescing operator behavior', () => {it('should demonstrate correct ?? behavior vs || behavior', () => {const testValues = [true, false, null, undefined, 0, '', NaN];testValues.forEach(value => {const orResult = value || null;const nullishResult = value ?? null;if (value === false) {// This is the key difference: false should stay false with ??expect(orResult).toBe(null); // || converts false to null (BAD)expect(nullishResult).toBe(false); // ?? keeps false as false (GOOD)} else if (value === true) {// Both operators should preserve trueexpect(orResult).toBe(true);expect(nullishResult).toBe(true);} else if (value === null || value === undefined) {// Both operators should convert null/undefined to nullexpect(orResult).toBe(null);expect(nullishResult).toBe(null);}});});it('should handle edge cases in boolean logic', () => {// Test various scenarios that might occur in real dataconst scenarios = [{ name: 'Explicit false', value: false, expected: false },{ name: 'Explicit true', value: true, expected: true },{ name: 'Null value', value: null, expected: null },{ name: 'Undefined value', value: undefined, expected: null },{ name: 'Zero value', value: 0, expected: 0 },{ name: 'Empty string', value: '', expected: '' },{ name: 'Non-empty string', value: 'false', expected: 'false' }];scenarios.forEach(({ value, expected }) => {const result = value ?? null;expect(result).toBe(expected);});});});});
/*** Tests for AudioMoth parser and file import data creation* Focuses on boolean field handling and astronomical data integration*/import { describe, it, expect, vi } from 'vitest';import { createFileImportData, parseAudioMothComment, isAudioMothFile } from '@/utils/audioMothParser';import { calculateAstronomicalData } from '@/utils/astronomicalCalculations';// Mock the calculateAstronomicalData function to control its outputvi.mock('@/utils/astronomicalCalculations', () => ({calculateAstronomicalData: vi.fn()}));// Mock file utilities for testingvi.mock('@/utils/fileUtils', () => ({processBasicFileInfo: vi.fn().mockResolvedValue({fileName: 'test.wav',xxh64Hash: 'abcd1234',duration: 60,sampleRate: 44100}),extractAudioMetadata: vi.fn().mockResolvedValue({fileName: 'test.wav',fileSize: 1000000,duration: 60,sampleRate: 44100,artist: 'TestArtist',comment: 'Test comment'})}));describe('AudioMoth Parser Boolean Logic', () => {const mockFile = new File(['test content'], 'test.wav', { type: 'audio/wav' });const locationId = 'test-location';const clusterId = 'test-cluster';const folderPath = '/test/path';const clusterLocation = { latitude: -36.8485, longitude: 174.7633 };beforeEach(() => {vi.clearAllMocks();});describe('createFileImportData boolean handling', () => {it('should preserve false values for maybeSolarNight and maybeCivilNight', async () => {// Mock astronomical calculation to return false values (daytime)const mockCalculateAstronomicalData = calculateAstronomicalData as ReturnType<typeof vi.fn>;mockCalculateAstronomicalData.mockReturnValue({maybeSolarNight: false, // This should NOT become nullmaybeCivilNight: false, // This should NOT become nullmoonPhase: 0.25});const result = await createFileImportData(mockFile,locationId,clusterId,undefined,folderPath,clusterLocation);// Critical test: false values should remain false, not become nullexpect(result).toHaveProperty('maybeSolarNight', false);expect(result).toHaveProperty('maybeCivilNight', false);expect(result.maybeSolarNight).not.toBe(null);expect(result.maybeCivilNight).not.toBe(null);expect(result).toHaveProperty('moonPhase', 0.25);});it('should preserve true values for maybeSolarNight and maybeCivilNight', async () => {// Mock astronomical calculation to return true values (nighttime)const mockCalculateAstronomicalData = calculateAstronomicalData as ReturnType<typeof vi.fn>;mockCalculateAstronomicalData.mockReturnValue({maybeSolarNight: true, // This should remain truemaybeCivilNight: true, // This should remain truemoonPhase: 0.75});const result = await createFileImportData(mockFile,locationId,clusterId,undefined,folderPath,clusterLocation);expect(result).toHaveProperty('maybeSolarNight', true);expect(result).toHaveProperty('maybeCivilNight', true);expect(result).toHaveProperty('moonPhase', 0.75);});it('should handle null when astronomical data is not available', async () => {// Test without cluster location (should result in null values)const result = await createFileImportData(mockFile,locationId,clusterId,undefined,folderPath,undefined // No cluster location);// When no location data is available, should be nullexpect(result).toHaveProperty('maybeSolarNight', null);expect(result).toHaveProperty('maybeCivilNight', null);expect(result).toHaveProperty('moonPhase', null);});it('should handle null when astronomical calculation fails', async () => {// Mock astronomical calculation to throw an errorconst mockCalculateAstronomicalData = calculateAstronomicalData as ReturnType<typeof vi.fn>;mockCalculateAstronomicalData.mockImplementation(() => {throw new Error('Calculation failed');});const result = await createFileImportData(mockFile,locationId,clusterId,undefined,folderPath,clusterLocation);// When calculation fails, should be nullexpect(result).toHaveProperty('maybeSolarNight', null);expect(result).toHaveProperty('maybeCivilNight', null);expect(result).toHaveProperty('moonPhase', null);});it('should include all required fields in the output', async () => {const mockCalculateAstronomicalData = calculateAstronomicalData as ReturnType<typeof vi.fn>;mockCalculateAstronomicalData.mockReturnValue({maybeSolarNight: true,maybeCivilNight: false,moonPhase: 0.5});const result = await createFileImportData(mockFile,locationId,clusterId,undefined,folderPath,clusterLocation);// Check all expected fields existexpect(result).toHaveProperty('fileName');expect(result).toHaveProperty('path', folderPath);expect(result).toHaveProperty('xxh64Hash');expect(result).toHaveProperty('locationId', locationId);expect(result).toHaveProperty('clusterId', clusterId);expect(result).toHaveProperty('duration');expect(result).toHaveProperty('sampleRate');expect(result).toHaveProperty('timestampLocal');expect(result).toHaveProperty('maybeSolarNight');expect(result).toHaveProperty('maybeCivilNight');expect(result).toHaveProperty('moonPhase');expect(result).toHaveProperty('upload', false);});});describe('Boolean logic regression tests', () => {it('should test the specific bug scenario: false || null vs false ?? null', () => {// Demonstrate the difference between || and ?? with boolean values// The old buggy logic (||) - using variables to avoid ESLint warningsconst falseValue = false;const trueValue = true;const undefinedValue = undefined;const buggyLogic = {maybeSolarNight: falseValue || null, // Results in null (BUG!)maybeCivilNight: trueValue || null, // Results in true (correct)undefinedValue: undefinedValue || null // Results in null (correct)};// The fixed logic (??)const fixedLogic = {maybeSolarNight: falseValue ?? null, // Results in false (FIXED!)maybeCivilNight: trueValue ?? null, // Results in true (correct)undefinedValue: undefinedValue ?? null // Results in null (correct)};// Demonstrate the bugexpect(buggyLogic.maybeSolarNight).toBe(null); // BUG: false became nullexpect(buggyLogic.maybeCivilNight).toBe(true); // OK: true stayed trueexpect(buggyLogic.undefinedValue).toBe(null); // OK: undefined became null// Demonstrate the fixexpect(fixedLogic.maybeSolarNight).toBe(false); // FIXED: false stays falseexpect(fixedLogic.maybeCivilNight).toBe(true); // OK: true stays trueexpect(fixedLogic.undefinedValue).toBe(null); // OK: undefined becomes null});it('should test various boolean combinations with nullish coalescing', () => {const testCases = [{ input: true, expected: true },{ input: false, expected: false },{ input: null, expected: null },{ input: undefined, expected: null }];testCases.forEach(({ input, expected }) => {const result = input ?? null;expect(result).toBe(expected);});});});});describe('AudioMoth Comment Parsing', () => {describe('parseAudioMothComment', () => {it('should parse a valid AudioMoth comment', () => {const comment = 'Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C.';const result = parseAudioMothComment(comment);expect(result).not.toBeNull();expect(result?.timestampLocal).toBe('2025-02-24T21:00:00.000+13:00');expect(result?.timestampUTC).toMatch(/^2025-02-24T08:00:00\.000Z$/);expect(result?.mothId).toBe('248AB50153AB0549');expect(result?.gain).toBe('medium');expect(result?.batteryV).toBe(4.3);expect(result?.tempC).toBe(15.8);});it('should return null for invalid comments', () => {const invalidComments = ['Not an AudioMoth comment','Recorded at invalid time format','Short comment','','AudioMoth without proper format'];invalidComments.forEach(comment => {const result = parseAudioMothComment(comment);expect(result).toBeNull();});});it('should handle different timezone formats', () => {const commentUTCMinus = 'Recorded at 10:30:45 15/06/2024 (UTC-05) by AudioMoth 123456789ABCDEF0 at high gain while battery was 3.9V and temperature was 22.1C.';const result = parseAudioMothComment(commentUTCMinus);expect(result).not.toBeNull();expect(result?.timestampLocal).toBe('2024-06-15T10:30:45.000-05:00');expect(result?.gain).toBe('high');expect(result?.batteryV).toBe(3.9);expect(result?.tempC).toBe(22.1);});});describe('isAudioMothFile', () => {it('should identify AudioMoth files by artist field', () => {expect(isAudioMothFile({ artist: 'AudioMoth' })).toBe(true);expect(isAudioMothFile({ artist: 'AudioMoth 123456' })).toBe(true);expect(isAudioMothFile({ artist: 'Other Artist' })).toBe(false);});it('should identify AudioMoth files by comment field', () => {expect(isAudioMothFile({ comment: 'Recorded by AudioMoth...' })).toBe(true);expect(isAudioMothFile({ comment: 'Regular recording comment' })).toBe(false);});it('should handle missing metadata', () => {expect(isAudioMothFile({})).toBe(false);expect(isAudioMothFile({ artist: undefined, comment: undefined })).toBe(false);expect(isAudioMothFile({ artist: '', comment: '' })).toBe(false);});});});
/*** Tests for astronomical calculations utilities* Tests SunCalc integration and boolean logic fixes*/import { describe, it, expect } from 'vitest';import { calculateAstronomicalData, batchCalculateAstronomicalData, formatMoonPhase, type ClusterLocation } from '@/utils/astronomicalCalculations';describe('Astronomical Calculations', () => {// Test location: Auckland, New Zealand (approx coordinates)const testLocation: ClusterLocation = {latitude: -36.8485,longitude: 174.7633};// Test location: London, UK (for different timezone testing)const londonLocation: ClusterLocation = {latitude: 51.5074,longitude: -0.1278};describe('calculateAstronomicalData', () => {it('should return true for solar night during nighttime hours', () => {// Winter midnight in Auckland (should be solar night)const winterMidnight = '2024-06-15T12:00:00.000Z'; // UTC midnight = noon in Auckland (winter)const duration = 60; // 1 minuteconst result = calculateAstronomicalData(winterMidnight, duration, testLocation);expect(result).toHaveProperty('maybeSolarNight');expect(result).toHaveProperty('maybeCivilNight');expect(result).toHaveProperty('moonPhase');expect(typeof result.maybeSolarNight).toBe('boolean');expect(typeof result.maybeCivilNight).toBe('boolean');expect(typeof result.moonPhase).toBe('number');});it('should return false for solar night during daytime hours', () => {// Summer midday in Auckland (should NOT be solar night)const summerMidday = '2024-12-15T00:00:00.000Z'; // UTC midnight = noon in Auckland (summer)const duration = 60; // 1 minuteconst result = calculateAstronomicalData(summerMidday, duration, testLocation);// During summer midday, should NOT be solar nightexpect(result.maybeSolarNight).toBe(false);expect(result.maybeCivilNight).toBe(false);});it('should handle different durations correctly', () => {const timestamp = '2024-06-15T10:00:00.000Z';const shortDuration = 30; // 30 secondsconst longDuration = 3600; // 1 hourconst shortResult = calculateAstronomicalData(timestamp, shortDuration, testLocation);const longResult = calculateAstronomicalData(timestamp, longDuration, testLocation);// Both should have valid resultsexpect(typeof shortResult.maybeSolarNight).toBe('boolean');expect(typeof longResult.maybeSolarNight).toBe('boolean');// Results might differ if the longer duration crosses sunrise/sunsetexpect(shortResult.moonPhase).toBeGreaterThanOrEqual(0);expect(shortResult.moonPhase).toBeLessThanOrEqual(1);expect(longResult.moonPhase).toBeGreaterThanOrEqual(0);expect(longResult.moonPhase).toBeLessThanOrEqual(1);});it('should calculate midpoint time correctly', () => {// Test that the calculation uses the midpoint, not the start timeconst startTime = '2024-06-15T10:00:00.000Z';const duration = 7200; // 2 hours (midpoint would be 1 hour later)const result = calculateAstronomicalData(startTime, duration, testLocation);// Should calculate based on 11:00 UTC, not 10:00 UTCexpect(typeof result.maybeSolarNight).toBe('boolean');expect(typeof result.maybeCivilNight).toBe('boolean');});it('should handle different geographical locations', () => {const timestamp = '2024-06-15T12:00:00.000Z'; // UTC noonconst duration = 60;const aucklandResult = calculateAstronomicalData(timestamp, duration, testLocation);const londonResult = calculateAstronomicalData(timestamp, duration, londonLocation);// Both should have valid boolean resultsexpect(typeof aucklandResult.maybeSolarNight).toBe('boolean');expect(typeof londonResult.maybeSolarNight).toBe('boolean');// Results might differ due to different timezones and seasons// Auckland: UTC noon = midnight local (winter) = likely night// London: UTC noon = 1pm local (summer) = likely day});it('should return valid moon phase values', () => {const timestamp = '2024-06-15T12:00:00.000Z';const duration = 60;const result = calculateAstronomicalData(timestamp, duration, testLocation);expect(result.moonPhase).toBeGreaterThanOrEqual(0);expect(result.moonPhase).toBeLessThanOrEqual(1);expect(typeof result.moonPhase).toBe('number');});it('should handle edge cases with very short durations', () => {const timestamp = '2024-06-15T12:00:00.000Z';const duration = 0.1; // 0.1 secondsconst result = calculateAstronomicalData(timestamp, duration, testLocation);expect(typeof result.maybeSolarNight).toBe('boolean');expect(typeof result.maybeCivilNight).toBe('boolean');expect(typeof result.moonPhase).toBe('number');});it('should handle edge cases with very long durations', () => {const timestamp = '2024-06-15T12:00:00.000Z';const duration = 86400; // 24 hoursconst result = calculateAstronomicalData(timestamp, duration, testLocation);expect(typeof result.maybeSolarNight).toBe('boolean');expect(typeof result.maybeCivilNight).toBe('boolean');expect(typeof result.moonPhase).toBe('number');});});describe('batchCalculateAstronomicalData', () => {it('should process multiple files correctly', () => {const files = [{ timestampUTC: '2024-06-15T10:00:00.000Z', durationSeconds: 60 },{ timestampUTC: '2024-06-15T14:00:00.000Z', durationSeconds: 120 },{ timestampUTC: '2024-06-15T18:00:00.000Z', durationSeconds: 30 }];const results = batchCalculateAstronomicalData(files, testLocation);expect(results).toHaveLength(3);results.forEach((result) => {expect(typeof result.maybeSolarNight).toBe('boolean');expect(typeof result.maybeCivilNight).toBe('boolean');expect(typeof result.moonPhase).toBe('number');expect(result.moonPhase).toBeGreaterThanOrEqual(0);expect(result.moonPhase).toBeLessThanOrEqual(1);});});it('should handle empty array', () => {const results = batchCalculateAstronomicalData([], testLocation);expect(results).toHaveLength(0);});});describe('formatMoonPhase', () => {it('should format moon phases correctly', () => {expect(formatMoonPhase(0)).toBe('New Moon (0%)');expect(formatMoonPhase(0.01)).toBe('Waxing Crescent (1%)');expect(formatMoonPhase(0.25)).toBe('First Quarter (25%)');expect(formatMoonPhase(0.45)).toBe('Waxing Gibbous (45%)');expect(formatMoonPhase(0.5)).toBe('Full Moon (50%)');expect(formatMoonPhase(0.65)).toBe('Waning Gibbous (65%)');expect(formatMoonPhase(0.75)).toBe('Last Quarter (75%)');expect(formatMoonPhase(0.85)).toBe('Waning Crescent (85%)');expect(formatMoonPhase(0.99)).toBe('New Moon (99%)');expect(formatMoonPhase(1)).toBe('New Moon (100%)');});it('should handle edge values', () => {expect(formatMoonPhase(0.001)).toBe('New Moon (0%)'); // < 0.01 = New Moonexpect(formatMoonPhase(0.01)).toBe('Waxing Crescent (1%)'); // >= 0.01 and < 0.24 = Waxing Crescentexpect(formatMoonPhase(0.48)).toBe('Waxing Gibbous (48%)'); // >= 0.26 and < 0.49 = Waxing Gibbousexpect(formatMoonPhase(0.5)).toBe('Full Moon (50%)'); // >= 0.49 and < 0.51 = Full Moonexpect(formatMoonPhase(0.52)).toBe('Waning Gibbous (52%)'); // >= 0.51 and < 0.74 = Waning Gibbous});});describe('Boolean Logic Validation', () => {it('should never return null for valid inputs', () => {const testCases = ['2024-06-15T06:00:00.000Z', // Dawn/dusk time'2024-06-15T12:00:00.000Z', // Midday/midnight'2024-06-15T18:00:00.000Z', // Evening/morning'2024-12-15T06:00:00.000Z', // Summer dawn/dusk'2024-12-15T12:00:00.000Z', // Summer midday/midnight'2024-12-15T18:00:00.000Z' // Summer evening/morning];testCases.forEach((timestamp) => {const result = calculateAstronomicalData(timestamp, 60, testLocation);// These should NEVER be null for valid inputsexpect(result.maybeSolarNight).not.toBe(null);expect(result.maybeCivilNight).not.toBe(null);expect(result.moonPhase).not.toBe(null);// They should be proper boolean/number typesexpect(typeof result.maybeSolarNight).toBe('boolean');expect(typeof result.maybeCivilNight).toBe('boolean');expect(typeof result.moonPhase).toBe('number');});});it('should return false for daytime recordings (not null)', () => {// Test a known daytime period in Auckland (summer midday UTC)const summerMidday = '2024-12-15T00:30:00.000Z'; // Should be daytime in Aucklandconst duration = 60;const result = calculateAstronomicalData(summerMidday, duration, testLocation);// The key test: false values should remain false, not become nullif (result.maybeSolarNight === false) {expect(result.maybeSolarNight).toBe(false);expect(result.maybeSolarNight).not.toBe(null);}if (result.maybeCivilNight === false) {expect(result.maybeCivilNight).toBe(false);expect(result.maybeCivilNight).not.toBe(null);}});it('should return true for nighttime recordings (not null)', () => {// Test a known nighttime period in Auckland (winter midnight UTC)const winterMidnight = '2024-06-15T12:30:00.000Z'; // Should be nighttime in Aucklandconst duration = 60;const result = calculateAstronomicalData(winterMidnight, duration, testLocation);// The key test: true values should remain trueif (result.maybeSolarNight === true) {expect(result.maybeSolarNight).toBe(true);expect(result.maybeSolarNight).not.toBe(null);}if (result.maybeCivilNight === true) {expect(result.maybeCivilNight).toBe(true);expect(result.maybeCivilNight).not.toBe(null);}});});});