mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-03 05:26:05 +02:00
feat: status bar
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
import { describe, expect, test, beforeEach } from 'vitest';
|
||||
|
||||
import {
|
||||
defaultVisibility,
|
||||
formatAppUptime,
|
||||
formatUtcHour,
|
||||
loadClockCount,
|
||||
loadClocks,
|
||||
loadVisibility,
|
||||
normalizeClock,
|
||||
normalizeUtcHour,
|
||||
parseClockOffset
|
||||
} from '../statusBarUtils';
|
||||
|
||||
// ─── normalizeUtcHour ────────────────────────────────────────────────
|
||||
|
||||
describe('normalizeUtcHour', () => {
|
||||
test('passes through normal integer values', () => {
|
||||
expect(normalizeUtcHour(0)).toBe(0);
|
||||
expect(normalizeUtcHour(5)).toBe(5);
|
||||
expect(normalizeUtcHour(-5)).toBe(-5);
|
||||
});
|
||||
|
||||
test('clamps to lower bound -12', () => {
|
||||
expect(normalizeUtcHour(-12)).toBe(-12);
|
||||
expect(normalizeUtcHour(-13)).toBe(-12);
|
||||
expect(normalizeUtcHour(-100)).toBe(-12);
|
||||
});
|
||||
|
||||
test('clamps to upper bound 14', () => {
|
||||
expect(normalizeUtcHour(14)).toBe(14);
|
||||
expect(normalizeUtcHour(15)).toBe(14);
|
||||
expect(normalizeUtcHour(100)).toBe(14);
|
||||
});
|
||||
|
||||
test('rounds fractional values', () => {
|
||||
expect(normalizeUtcHour(5.4)).toBe(5);
|
||||
expect(normalizeUtcHour(5.5)).toBe(6);
|
||||
expect(normalizeUtcHour(-5.7)).toBe(-6);
|
||||
});
|
||||
|
||||
test('returns 0 for NaN and Infinity', () => {
|
||||
expect(normalizeUtcHour(NaN)).toBe(0);
|
||||
expect(normalizeUtcHour(Infinity)).toBe(0);
|
||||
expect(normalizeUtcHour(-Infinity)).toBe(0);
|
||||
});
|
||||
|
||||
test('coerces string numbers', () => {
|
||||
expect(normalizeUtcHour('9')).toBe(9);
|
||||
expect(normalizeUtcHour('-3')).toBe(-3);
|
||||
});
|
||||
|
||||
test('returns 0 for non-numeric strings', () => {
|
||||
expect(normalizeUtcHour('abc')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── formatUtcHour ───────────────────────────────────────────────────
|
||||
|
||||
describe('formatUtcHour', () => {
|
||||
test('formats positive offsets with plus sign', () => {
|
||||
expect(formatUtcHour(9)).toBe('UTC+9');
|
||||
expect(formatUtcHour(14)).toBe('UTC+14');
|
||||
});
|
||||
|
||||
test('formats negative offsets', () => {
|
||||
expect(formatUtcHour(-5)).toBe('UTC-5');
|
||||
expect(formatUtcHour(-12)).toBe('UTC-12');
|
||||
});
|
||||
|
||||
test('formats zero as positive', () => {
|
||||
expect(formatUtcHour(0)).toBe('UTC+0');
|
||||
});
|
||||
|
||||
test('normalises before formatting', () => {
|
||||
expect(formatUtcHour(20)).toBe('UTC+14');
|
||||
expect(formatUtcHour(-20)).toBe('UTC-12');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── parseClockOffset ────────────────────────────────────────────────
|
||||
|
||||
describe('parseClockOffset', () => {
|
||||
test('parses numeric input', () => {
|
||||
expect(parseClockOffset(9)).toBe(9);
|
||||
expect(parseClockOffset(-3)).toBe(-3);
|
||||
});
|
||||
|
||||
test('parses plain numeric strings', () => {
|
||||
expect(parseClockOffset('5')).toBe(5);
|
||||
expect(parseClockOffset('-7')).toBe(-7);
|
||||
expect(parseClockOffset('+3')).toBe(3);
|
||||
});
|
||||
|
||||
test('parses numeric strings with whitespace', () => {
|
||||
expect(parseClockOffset(' 5 ')).toBe(5);
|
||||
});
|
||||
|
||||
test('parses UTC+N pattern', () => {
|
||||
expect(parseClockOffset('UTC+9')).toBe(9);
|
||||
expect(parseClockOffset('UTC-5')).toBe(-5);
|
||||
expect(parseClockOffset('utc+0')).toBe(0);
|
||||
});
|
||||
|
||||
test('parses UTC pattern with half-hour offset', () => {
|
||||
expect(parseClockOffset('UTC+5:30')).toBe(6); // 5.5 rounds to 6
|
||||
expect(parseClockOffset('UTC-9:30')).toBe(-9); // -9.5 rounds to -9 (Math.round toward +Infinity)
|
||||
});
|
||||
|
||||
test('returns 0 for non-string non-number input', () => {
|
||||
expect(parseClockOffset(null)).toBe(0);
|
||||
expect(parseClockOffset(undefined)).toBe(0);
|
||||
expect(parseClockOffset(true)).toBe(0);
|
||||
expect(parseClockOffset([])).toBe(0);
|
||||
});
|
||||
|
||||
test('returns 0 for unrecognised string patterns', () => {
|
||||
expect(parseClockOffset('not-a-timezone')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── normalizeClock ──────────────────────────────────────────────────
|
||||
|
||||
describe('normalizeClock', () => {
|
||||
test('normalises entry with offset key', () => {
|
||||
expect(normalizeClock({ offset: 9 })).toEqual({ offset: 9 });
|
||||
expect(normalizeClock({ offset: '5' })).toEqual({ offset: 5 });
|
||||
});
|
||||
|
||||
test('normalises legacy entry with timezone key', () => {
|
||||
expect(normalizeClock({ timezone: 'UTC+9' })).toEqual({ offset: 9 });
|
||||
});
|
||||
|
||||
test('prefers offset over timezone when both present', () => {
|
||||
expect(normalizeClock({ offset: 3, timezone: 'UTC+9' })).toEqual({
|
||||
offset: 3
|
||||
});
|
||||
});
|
||||
|
||||
test('returns { offset: 0 } for non-object input', () => {
|
||||
expect(normalizeClock(null)).toEqual({ offset: 0 });
|
||||
expect(normalizeClock(undefined)).toEqual({ offset: 0 });
|
||||
expect(normalizeClock(42)).toEqual({ offset: 0 });
|
||||
expect(normalizeClock('string')).toEqual({ offset: 0 });
|
||||
});
|
||||
|
||||
test('returns { offset: 0 } for object without known keys', () => {
|
||||
expect(normalizeClock({ foo: 'bar' })).toEqual({ offset: 0 });
|
||||
expect(normalizeClock({})).toEqual({ offset: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── loadVisibility ──────────────────────────────────────────────────
|
||||
|
||||
describe('loadVisibility', () => {
|
||||
let storage;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = createMockStorage();
|
||||
});
|
||||
|
||||
test('returns defaults when storage is empty', () => {
|
||||
expect(loadVisibility(storage)).toEqual(defaultVisibility);
|
||||
});
|
||||
|
||||
test('merges saved values with defaults', () => {
|
||||
storage.setItem(
|
||||
'VRCX_statusBarVisibility',
|
||||
JSON.stringify({ vrchat: false, ws: false })
|
||||
);
|
||||
const result = loadVisibility(storage);
|
||||
expect(result.vrchat).toBe(false);
|
||||
expect(result.ws).toBe(false);
|
||||
// Other defaults preserved
|
||||
expect(result.proxy).toBe(true);
|
||||
expect(result.zoom).toBe(true);
|
||||
});
|
||||
|
||||
test('returns defaults on corrupt JSON', () => {
|
||||
storage.setItem('VRCX_statusBarVisibility', '{bad-json');
|
||||
expect(loadVisibility(storage)).toEqual(defaultVisibility);
|
||||
});
|
||||
|
||||
test('returns a new object each time (no shared reference)', () => {
|
||||
const a = loadVisibility(storage);
|
||||
const b = loadVisibility(storage);
|
||||
expect(a).not.toBe(b);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── loadClocks ──────────────────────────────────────────────────────
|
||||
|
||||
describe('loadClocks', () => {
|
||||
const defaults = [{ offset: 9 }, { offset: 0 }, { offset: -5 }];
|
||||
let storage;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = createMockStorage();
|
||||
});
|
||||
|
||||
test('returns defaults when storage is empty', () => {
|
||||
const result = loadClocks(storage, defaults);
|
||||
expect(result).toEqual(defaults);
|
||||
});
|
||||
|
||||
test('loads valid saved clocks', () => {
|
||||
storage.setItem(
|
||||
'VRCX_statusBarClocks',
|
||||
JSON.stringify([{ offset: 1 }, { offset: 2 }, { offset: 3 }])
|
||||
);
|
||||
expect(loadClocks(storage, defaults)).toEqual([
|
||||
{ offset: 1 },
|
||||
{ offset: 2 },
|
||||
{ offset: 3 }
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns defaults for wrong array length', () => {
|
||||
storage.setItem(
|
||||
'VRCX_statusBarClocks',
|
||||
JSON.stringify([{ offset: 1 }])
|
||||
);
|
||||
expect(loadClocks(storage, defaults)).toEqual(defaults);
|
||||
});
|
||||
|
||||
test('returns defaults for non-array JSON', () => {
|
||||
storage.setItem('VRCX_statusBarClocks', JSON.stringify({ offset: 1 }));
|
||||
expect(loadClocks(storage, defaults)).toEqual(defaults);
|
||||
});
|
||||
|
||||
test('returns defaults on corrupt JSON', () => {
|
||||
storage.setItem('VRCX_statusBarClocks', 'not-json');
|
||||
expect(loadClocks(storage, defaults)).toEqual(defaults);
|
||||
});
|
||||
|
||||
test('normalises clock entries from storage', () => {
|
||||
storage.setItem(
|
||||
'VRCX_statusBarClocks',
|
||||
JSON.stringify([
|
||||
{ offset: '5' },
|
||||
{ timezone: 'UTC+3' },
|
||||
{ offset: 99 }
|
||||
])
|
||||
);
|
||||
expect(loadClocks(storage, defaults)).toEqual([
|
||||
{ offset: 5 },
|
||||
{ offset: 3 },
|
||||
{ offset: 14 } // clamped
|
||||
]);
|
||||
});
|
||||
|
||||
test('returned defaults are independent copies', () => {
|
||||
const a = loadClocks(storage, defaults);
|
||||
const b = loadClocks(storage, defaults);
|
||||
expect(a).not.toBe(b);
|
||||
a[0].offset = 999;
|
||||
expect(b[0].offset).not.toBe(999);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── loadClockCount ──────────────────────────────────────────────────
|
||||
|
||||
describe('loadClockCount', () => {
|
||||
let storage;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = createMockStorage();
|
||||
});
|
||||
|
||||
test('returns 3 when storage is empty', () => {
|
||||
expect(loadClockCount(storage)).toBe(3);
|
||||
});
|
||||
|
||||
test.each([0, 1, 2, 3])('returns valid stored count %i', (n) => {
|
||||
storage.setItem('VRCX_statusBarClockCount', String(n));
|
||||
expect(loadClockCount(storage)).toBe(n);
|
||||
});
|
||||
|
||||
test('returns 3 for out-of-range values', () => {
|
||||
storage.setItem('VRCX_statusBarClockCount', '4');
|
||||
expect(loadClockCount(storage)).toBe(3);
|
||||
|
||||
storage.setItem('VRCX_statusBarClockCount', '-1');
|
||||
expect(loadClockCount(storage)).toBe(3);
|
||||
});
|
||||
|
||||
test('returns 3 for non-numeric values', () => {
|
||||
storage.setItem('VRCX_statusBarClockCount', 'abc');
|
||||
expect(loadClockCount(storage)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── formatAppUptime ─────────────────────────────────────────────────
|
||||
|
||||
describe('formatAppUptime', () => {
|
||||
test('formats zero seconds', () => {
|
||||
expect(formatAppUptime(0)).toBe('00:00:00');
|
||||
});
|
||||
|
||||
test('formats seconds only', () => {
|
||||
expect(formatAppUptime(45)).toBe('00:00:45');
|
||||
});
|
||||
|
||||
test('formats minutes and seconds', () => {
|
||||
expect(formatAppUptime(125)).toBe('00:02:05');
|
||||
});
|
||||
|
||||
test('formats hours, minutes, and seconds', () => {
|
||||
expect(formatAppUptime(3661)).toBe('01:01:01');
|
||||
});
|
||||
|
||||
test('handles large values (over 24 hours)', () => {
|
||||
// 100 hours = 360000 seconds
|
||||
expect(formatAppUptime(360000)).toBe('100:00:00');
|
||||
});
|
||||
|
||||
test('treats negative values as zero', () => {
|
||||
expect(formatAppUptime(-10)).toBe('00:00:00');
|
||||
});
|
||||
|
||||
test('floors fractional seconds', () => {
|
||||
expect(formatAppUptime(59.9)).toBe('00:00:59');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── test helpers ────────────────────────────────────────────────────
|
||||
|
||||
/** Minimal in-memory Storage-like object for testing. */
|
||||
function createMockStorage() {
|
||||
const data = new Map();
|
||||
return {
|
||||
getItem: (key) => (data.has(key) ? data.get(key) : null),
|
||||
setItem: (key, value) => data.set(key, String(value)),
|
||||
removeItem: (key) => data.delete(key),
|
||||
clear: () => data.clear()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user