Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coverage #166

Merged
merged 4 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/mathUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function toDirection(orientation) {
direction += 360;
}

return direction;
return direction === 0 ? 0 : direction;
}

/**
Expand Down
109 changes: 107 additions & 2 deletions src/tests/lib/formatters.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { convertUnixToTime } from '$lib/formatters';
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { convertUnixToTime, formatLastUpdated, formatTime } from '$lib/formatters';

describe('convertUnixToTime', () => {
it('returns a blank string when its input is null', () => {
Expand All @@ -14,3 +14,108 @@ describe('convertUnixToTime', () => {
expect(convertUnixToTime(1727442050)).toBe('01:00 PM');
});
});

describe('formatLastUpdated', () => {
const translations = {
min: 'min',
sec: 'sec',
ago: 'ago'
};

beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-16T12:00:00Z'));
});

afterEach(() => {
vi.useRealTimers();
});

it('formats time less than a minute ago', () => {
const timestamp = new Date('2024-01-16T11:59:30Z');
const result = formatLastUpdated(timestamp, translations);
expect(result).toBe('30 sec ago');
});

it('formats time more than a minute ago', () => {
const timestamp = new Date('2024-01-16T11:58:30Z');
const result = formatLastUpdated(timestamp, translations);
expect(result).toBe('1 min 30 sec ago');
});

it('formats time multiple minutes ago', () => {
const timestamp = new Date('2024-01-16T11:57:15Z');
const result = formatLastUpdated(timestamp, translations);
expect(result).toBe('2 min 45 sec ago');
});

it('handles just-now timestamps', () => {
const timestamp = new Date('2024-01-16T11:59:59Z');
const result = formatLastUpdated(timestamp, translations);
expect(result).toBe('1 sec ago');
});

it('works with different translation objects', () => {
const spanishTranslations = {
min: 'min',
sec: 'seg',
ago: 'atrás'
};
const timestamp = new Date('2024-01-16T11:58:30Z');
const result = formatLastUpdated(timestamp, spanishTranslations);
expect(result).toBe('1 min 30 seg atrás');
});

it('handles zero seconds case', () => {
const timestamp = new Date('2024-01-16T12:00:00Z');
const result = formatLastUpdated(timestamp, translations);
expect(result).toBe('0 sec ago');
});
});

describe('formatTime', () => {
beforeEach(() => {
// Set timezone to UTC to avoid local timezone issues
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-16T12:00:00Z'));
// Mock toLocaleTimeString to ensure consistent output
vi.spyOn(Date.prototype, 'toLocaleTimeString').mockImplementation(function () {
const hours = this.getUTCHours();
const minutes = this.getUTCMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
const hour12 = hours % 12 || 12;
const minutesPadded = minutes.toString().padStart(2, '0');
return `${hour12}:${minutesPadded} ${ampm}`;
});
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

it('handles morning times', () => {
const result = formatTime('2024-01-16T09:15:00Z');
expect(result).toBe('9:15 AM');
});

it('handles afternoon times', () => {
const result = formatTime('2024-01-16T14:45:00Z');
expect(result).toBe('2:45 PM');
});

it('handles midnight', () => {
const result = formatTime('2024-01-16T00:00:00Z');
expect(result).toBe('12:00 AM');
});

it('handles noon', () => {
const result = formatTime('2024-01-16T12:00:00Z');
expect(result).toBe('12:00 PM');
});

it('pads minutes with leading zeros', () => {
const result = formatTime('2024-01-16T09:05:00Z');
expect(result).toBe('9:05 AM');
});
});
158 changes: 158 additions & 0 deletions src/tests/lib/keybinding.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { keybinding } from '$lib/keybinding';

describe('keybinding action', () => {
let node;
let mockWindow;
let currentHandler;

beforeEach(() => {
node = {
click: vi.fn()
};

mockWindow = {
addEventListener: vi.fn((event, handler) => {
if (event === 'keydown') {
currentHandler = handler;
}
}),
removeEventListener: vi.fn((event, handler) => {
if (event === 'keydown' && handler === currentHandler) {
currentHandler = null;
}
})
};

// Set up mock window
const originalWindow = global.window;
global.window = mockWindow;

beforeEach.cleanup = () => {
global.window = originalWindow;
};
});

afterEach(() => {
beforeEach.cleanup?.();
vi.clearAllMocks();
currentHandler = null;
});

const triggerKeydown = (keyParams) => {
const event = {
code: keyParams.code || '',
altKey: keyParams.alt || false,
shiftKey: keyParams.shift || false,
ctrlKey: keyParams.control || false,
metaKey: keyParams.control || false,
preventDefault: vi.fn()
};

if (currentHandler) {
currentHandler(event);
}
return event;
};

it('adds keydown event listener on initialization', () => {
keybinding(node, { code: 'KeyA' });
expect(mockWindow.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function));
});

it('removes existing listener before adding new one on update', () => {
let params = { code: 'KeyA' };
const action = keybinding(node, params);

// Clear the initial setup calls
mockWindow.addEventListener.mockClear();
mockWindow.removeEventListener.mockClear();

// Update the params object directly
params.code = 'KeyB';
action.update();

expect(mockWindow.removeEventListener).toHaveBeenCalledTimes(1);
expect(mockWindow.addEventListener).toHaveBeenCalledTimes(1);
});

it('removes listener on destroy', () => {
const action = keybinding(node, { code: 'KeyA' });
action.destroy();

expect(mockWindow.removeEventListener).toHaveBeenCalled();
});

it('only responds to matching key code', () => {
keybinding(node, { code: 'KeyA' });

triggerKeydown({ code: 'KeyB' });
expect(node.click).not.toHaveBeenCalled();

triggerKeydown({ code: 'KeyA' });
expect(node.click).toHaveBeenCalledTimes(1);
});

it('responds to updated key code', () => {
// Create params object that we'll modify
let params = { code: 'KeyA' };
const action = keybinding(node, params);

// Verify initial binding works
triggerKeydown({ code: 'KeyA' });
expect(node.click).toHaveBeenCalledTimes(1);

// Update params and trigger update
node.click.mockClear();
params.code = 'KeyB';
action.update();

// Verify new binding works
triggerKeydown({ code: 'KeyB' });
expect(node.click).toHaveBeenCalledTimes(1);

// Verify old binding doesn't work
node.click.mockClear();
triggerKeydown({ code: 'KeyA' });
expect(node.click).not.toHaveBeenCalled();
});

it('handles modifier keys correctly', () => {
keybinding(node, {
code: 'KeyA',
alt: true,
shift: true,
control: true
});

triggerKeydown({ code: 'KeyA' });
expect(node.click).not.toHaveBeenCalled();

triggerKeydown({ code: 'KeyA', alt: true });
expect(node.click).not.toHaveBeenCalled();

triggerKeydown({ code: 'KeyA', alt: true, shift: true, control: true });
expect(node.click).toHaveBeenCalledTimes(1);
});

it('handles meta key as control', () => {
keybinding(node, {
code: 'KeyA',
control: true
});

const event = triggerKeydown({ code: 'KeyA', control: true });
expect(node.click).toHaveBeenCalledTimes(1);
expect(event.preventDefault).toHaveBeenCalled();
});

it('triggers callback instead of click when provided', () => {
const callback = vi.fn();
keybinding(node, { code: 'KeyA', callback });

triggerKeydown({ code: 'KeyA' });

expect(callback).toHaveBeenCalledTimes(1);
expect(node.click).not.toHaveBeenCalled();
});
});
99 changes: 99 additions & 0 deletions src/tests/lib/mathUtils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, it, expect } from 'vitest';
import { toDirection, calculateMidpoint } from '$lib/mathUtils';

describe('toDirection', () => {
it('converts east orientation (0°) to 90° direction', () => {
expect(toDirection(0)).toBe(90);
});

it('converts north orientation (90°) to 0° direction', () => {
expect(toDirection(90)).toBe(0);
});

it('converts west orientation (180°) to 270° direction', () => {
expect(toDirection(180)).toBe(270);
});

it('converts south orientation (270°) to 180° direction', () => {
expect(toDirection(270)).toBe(180);
});

it('handles negative orientations', () => {
expect(toDirection(-90)).toBe(180); // -90° orientation should be 180° direction
expect(toDirection(-180)).toBe(270); // -180° orientation should be 270° direction
expect(toDirection(-270)).toBe(0); // -270° orientation should be 0° direction
});

it('handles orientations > 360°', () => {
expect(toDirection(450)).toBe(0); // 450° orientation (90° + 360°) should be 0° direction
expect(toDirection(720)).toBe(90); // 720° orientation (0° + 2*360°) should be 90° direction
});

it('converts arbitrary angles correctly', () => {
expect(toDirection(45)).toBe(45); // 45° orientation to 45° direction
expect(toDirection(135)).toBe(315); // 135° orientation to 315° direction
expect(toDirection(225)).toBe(225); // 225° orientation to 225° direction
expect(toDirection(315)).toBe(135); // 315° orientation to 135° direction
});
});

describe('calculateMidpoint', () => {
it('calculates midpoint for two stops', () => {
const stops = [
{ lat: 47.6062, lon: -122.3321 },
{ lat: 47.6092, lon: -122.3331 }
];

const result = calculateMidpoint(stops);

expect(result.lat).toBeCloseTo(47.6077);
expect(result.lng).toBeCloseTo(-122.3326);
});

it('calculates midpoint for multiple stops', () => {
const stops = [
{ lat: 47.6062, lon: -122.3321 },
{ lat: 47.6092, lon: -122.3331 },
{ lat: 47.6082, lon: -122.3341 }
];

const result = calculateMidpoint(stops);

expect(result.lat).toBeCloseTo(47.6079);
expect(result.lng).toBeCloseTo(-122.3331);
});

it('returns same point for single stop', () => {
const stops = [{ lat: 47.6062, lon: -122.3321 }];

const result = calculateMidpoint(stops);

expect(result.lat).toBe(47.6062);
expect(result.lng).toBe(-122.3321);
});

it('handles positive and negative coordinates', () => {
const stops = [
{ lat: -33.8688, lon: 151.2093 }, // Sydney
{ lat: 40.7128, lon: -74.006 } // New York
];

const result = calculateMidpoint(stops);

expect(result.lat).toBeCloseTo(3.422);
expect(result.lng).toBeCloseTo(38.6017);
});

it('handles coordinates around the same area', () => {
const stops = [
{ lat: 47.6062, lon: -122.3321 },
{ lat: 47.6065, lon: -122.3324 },
{ lat: 47.6068, lon: -122.3327 }
];

const result = calculateMidpoint(stops);

expect(result.lat).toBeCloseTo(47.6065);
expect(result.lng).toBeCloseTo(-122.3324);
});
});
Loading