diff --git a/src/lib/__tests__/credentials.test.ts b/src/lib/__tests__/credentials.test.ts new file mode 100644 index 0000000..c61cf28 --- /dev/null +++ b/src/lib/__tests__/credentials.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getStoredCredentials, storeCredentials, clearCredentials } from '../credentials'; + +// Mock localStorage +const store: Record = {}; +const localStorageMock = { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { store[key] = value; }), + removeItem: vi.fn((key: string) => { delete store[key]; }), +}; + +Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock }); + +beforeEach(() => { + for (const key of Object.keys(store)) delete store[key]; + vi.clearAllMocks(); +}); + +describe('getStoredCredentials', () => { + it('returns null when nothing stored', () => { + expect(getStoredCredentials()).toBeNull(); + }); + + it('returns credentials when valid JSON stored', () => { + store['pinchchat_credentials'] = JSON.stringify({ url: 'wss://gw.test', token: 'abc' }); + expect(getStoredCredentials()).toEqual({ url: 'wss://gw.test', token: 'abc' }); + }); + + it('returns null for malformed JSON', () => { + store['pinchchat_credentials'] = 'not-json'; + expect(getStoredCredentials()).toBeNull(); + }); + + it('returns null if url is missing', () => { + store['pinchchat_credentials'] = JSON.stringify({ token: 'abc' }); + expect(getStoredCredentials()).toBeNull(); + }); + + it('returns null if token is missing', () => { + store['pinchchat_credentials'] = JSON.stringify({ url: 'wss://gw' }); + expect(getStoredCredentials()).toBeNull(); + }); +}); + +describe('storeCredentials', () => { + it('stores credentials as JSON', () => { + storeCredentials('wss://gw', 'tok'); + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'pinchchat_credentials', + JSON.stringify({ url: 'wss://gw', token: 'tok' }), + ); + }); +}); + +describe('clearCredentials', () => { + it('removes the key from localStorage', () => { + store['pinchchat_credentials'] = 'something'; + clearCredentials(); + expect(localStorageMock.removeItem).toHaveBeenCalledWith('pinchchat_credentials'); + }); +}); diff --git a/src/lib/__tests__/image.test.ts b/src/lib/__tests__/image.test.ts new file mode 100644 index 0000000..12c2234 --- /dev/null +++ b/src/lib/__tests__/image.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { buildImageSrc } from '../image'; + +describe('buildImageSrc', () => { + it('returns URL when url is provided', () => { + expect(buildImageSrc('image/png', undefined, 'https://example.com/img.png')) + .toBe('https://example.com/img.png'); + }); + + it('prefers url over base64 data', () => { + expect(buildImageSrc('image/png', 'abc123', 'https://example.com/img.png')) + .toBe('https://example.com/img.png'); + }); + + it('builds data URL from base64 data', () => { + expect(buildImageSrc('image/png', 'abc123')) + .toBe('data:image/png;base64,abc123'); + }); + + it('builds data URL for jpeg', () => { + expect(buildImageSrc('image/jpeg', 'xyz')) + .toBe('data:image/jpeg;base64,xyz'); + }); + + it('returns empty string when neither url nor data provided', () => { + expect(buildImageSrc('image/png')).toBe(''); + }); + + it('returns empty string with undefined data and no url', () => { + expect(buildImageSrc('image/webp', undefined, undefined)).toBe(''); + }); +}); diff --git a/src/lib/__tests__/utils.test.ts b/src/lib/__tests__/utils.test.ts new file mode 100644 index 0000000..e89ddea --- /dev/null +++ b/src/lib/__tests__/utils.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { cn, genId, genIdempotencyKey } from '../utils'; + +describe('cn', () => { + it('merges class names', () => { + expect(cn('foo', 'bar')).toBe('foo bar'); + }); + + it('handles conditional classes', () => { + const showHidden = false; + expect(cn('base', showHidden && 'hidden', 'extra')).toBe('base extra'); + }); + + it('resolves tailwind conflicts (twMerge)', () => { + // twMerge deduplicates conflicting tailwind utilities + expect(cn('p-4', 'p-2')).toBe('p-2'); + }); + + it('handles empty input', () => { + expect(cn()).toBe(''); + }); + + it('handles undefined and null values', () => { + expect(cn('a', undefined, null, 'b')).toBe('a b'); + }); +}); + +describe('genId', () => { + it('generates unique ids with default prefix', () => { + const a = genId(); + const b = genId(); + expect(a).toMatch(/^req-\d+-\d+$/); + expect(a).not.toBe(b); + }); + + it('uses custom prefix', () => { + expect(genId('msg')).toMatch(/^msg-\d+-\d+$/); + }); +}); + +describe('genIdempotencyKey', () => { + it('returns a non-empty string', () => { + const key = genIdempotencyKey(); + expect(key.length).toBeGreaterThan(0); + }); + + it('generates unique keys', () => { + const a = genIdempotencyKey(); + const b = genIdempotencyKey(); + expect(a).not.toBe(b); + }); +});