- IndexedDB data layer: db.ts, items.ts, locations.ts, sightings.ts, labels.ts - Pure algorithms: confidence decay, custody state transitions - Barcode parser: HTTPS URI, haus:// scheme, raw ID parsing - 150 tests all passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
4.2 KiB
TypeScript
150 lines
4.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { parseItemId } from '$lib/scanning/parser';
|
|
import { generateItemId, generateSightingId } from '$lib/utils/id';
|
|
|
|
describe('parseItemId', () => {
|
|
describe('HTTPS URI format', () => {
|
|
it('parses a valid HTTPS URI', () => {
|
|
expect(parseItemId('https://haus.toph.so/abc2345')).toBe('abc2345');
|
|
});
|
|
|
|
it('parses a valid HTTPS URI with different valid ID', () => {
|
|
expect(parseItemId('https://haus.toph.so/mn3pqr7')).toBe('mn3pqr7');
|
|
});
|
|
|
|
it('returns null for HTTPS URI with invalid characters in ID', () => {
|
|
expect(parseItemId('https://haus.toph.so/abc1lO0')).toBeNull();
|
|
});
|
|
|
|
it('returns null for HTTPS URI with wrong length ID', () => {
|
|
expect(parseItemId('https://haus.toph.so/abc23')).toBeNull();
|
|
});
|
|
|
|
it('returns null for wrong domain', () => {
|
|
expect(parseItemId('https://example.com/abc2345')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('custom scheme format', () => {
|
|
it('parses a valid haus:// URI', () => {
|
|
expect(parseItemId('haus://abc2345')).toBe('abc2345');
|
|
});
|
|
|
|
it('returns null for haus:// URI with invalid characters', () => {
|
|
expect(parseItemId('haus://abc1lO0')).toBeNull();
|
|
});
|
|
|
|
it('returns null for haus:// URI with wrong length', () => {
|
|
expect(parseItemId('haus://abc23456789')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('raw ID format', () => {
|
|
it('parses a valid raw 7-char ID', () => {
|
|
expect(parseItemId('abc2345')).toBe('abc2345');
|
|
});
|
|
|
|
it('accepts all valid alphabet characters', () => {
|
|
// Alphabet: 23456789abcdefghjkmnpqrstuvwxyz
|
|
expect(parseItemId('2345678')).toBe('2345678');
|
|
expect(parseItemId('abcdefg')).toBe('abcdefg');
|
|
expect(parseItemId('hjkmnpq')).toBe('hjkmnpq');
|
|
expect(parseItemId('rstuvwx')).toBe('rstuvwx');
|
|
});
|
|
|
|
it('rejects excluded characters (0, 1, i, l, o)', () => {
|
|
expect(parseItemId('abc0def')).toBeNull();
|
|
expect(parseItemId('abc1def')).toBeNull();
|
|
expect(parseItemId('abcidef')).toBeNull();
|
|
expect(parseItemId('abcldef')).toBeNull();
|
|
expect(parseItemId('abcodef')).toBeNull();
|
|
});
|
|
|
|
it('rejects uppercase characters', () => {
|
|
expect(parseItemId('ABCdefg')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('invalid inputs', () => {
|
|
it('returns null for empty string', () => {
|
|
expect(parseItemId('')).toBeNull();
|
|
});
|
|
|
|
it('returns null for wrong length (too short)', () => {
|
|
expect(parseItemId('abc23')).toBeNull();
|
|
});
|
|
|
|
it('returns null for wrong length (too long)', () => {
|
|
expect(parseItemId('abc23456')).toBeNull();
|
|
});
|
|
|
|
it('returns null for string with spaces', () => {
|
|
expect(parseItemId('abc 234')).toBeNull();
|
|
});
|
|
|
|
it('returns null for string with special characters', () => {
|
|
expect(parseItemId('abc-234')).toBeNull();
|
|
});
|
|
|
|
it('returns null for completely invalid input', () => {
|
|
expect(parseItemId('not a barcode at all')).toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('generateItemId', () => {
|
|
const VALID_CHARS = /^[23456789a-hjkmnp-z]+$/;
|
|
|
|
it('generates a 7-character string', () => {
|
|
const id = generateItemId();
|
|
expect(id).toHaveLength(7);
|
|
});
|
|
|
|
it('uses only valid alphabet characters', () => {
|
|
for (let i = 0; i < 50; i++) {
|
|
const id = generateItemId();
|
|
expect(id).toMatch(VALID_CHARS);
|
|
}
|
|
});
|
|
|
|
it('does not contain ambiguous characters (0, 1, i, l, o)', () => {
|
|
for (let i = 0; i < 50; i++) {
|
|
const id = generateItemId();
|
|
expect(id).not.toMatch(/[01ilo]/);
|
|
}
|
|
});
|
|
|
|
it('generates unique IDs across multiple calls', () => {
|
|
const ids = new Set<string>();
|
|
for (let i = 0; i < 1000; i++) {
|
|
ids.add(generateItemId());
|
|
}
|
|
expect(ids.size).toBe(1000);
|
|
});
|
|
});
|
|
|
|
describe('generateSightingId', () => {
|
|
it('starts with "s_" prefix', () => {
|
|
const id = generateSightingId();
|
|
expect(id.startsWith('s_')).toBe(true);
|
|
});
|
|
|
|
it('has total length of 9 (2 prefix + 7 ID)', () => {
|
|
const id = generateSightingId();
|
|
expect(id).toHaveLength(9);
|
|
});
|
|
|
|
it('has a valid item ID after the prefix', () => {
|
|
const id = generateSightingId();
|
|
const suffix = id.slice(2);
|
|
expect(suffix).toMatch(/^[23456789a-hjkmnp-z]{7}$/);
|
|
});
|
|
|
|
it('generates unique IDs across multiple calls', () => {
|
|
const ids = new Set<string>();
|
|
for (let i = 0; i < 100; i++) {
|
|
ids.add(generateSightingId());
|
|
}
|
|
expect(ids.size).toBe(100);
|
|
});
|
|
});
|