Skip to content

Commit

Permalink
Merge pull request #166 from OneBusAway/coverage
Browse files Browse the repository at this point in the history
Coverage
  • Loading branch information
aaronbrethorst authored Jan 17, 2025
2 parents 5e9e43d + 9d743c9 commit 7bbfcc2
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 3 deletions.
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);
});
});

0 comments on commit 7bbfcc2

Please sign in to comment.