feat: status bar

This commit is contained in:
pa
2026-03-07 01:35:03 +09:00
parent c42b126131
commit e4f0abe74a
9 changed files with 1240 additions and 49 deletions
@@ -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()
};
}