This commit is contained in:
+27
-25
@@ -1,45 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readIdentity, writeIdentity } from './identity'
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readIdentity, writeIdentity } from './identity';
|
||||
|
||||
function createMockStorage(): Storage {
|
||||
const store = new Map<string, string>()
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void store.set(key, value),
|
||||
removeItem: (key: string) => void store.delete(key),
|
||||
clear: () => store.clear(),
|
||||
key: () => null,
|
||||
get length() { return store.size },
|
||||
}
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('identity storage', () => {
|
||||
it('returns null when nothing is stored', () => {
|
||||
expect(readIdentity(createMockStorage())).toBeNull()
|
||||
})
|
||||
expect(readIdentity(createMockStorage())).toBeNull();
|
||||
});
|
||||
|
||||
it('round-trips a written identity', () => {
|
||||
const storage = createMockStorage()
|
||||
writeIdentity(storage, { name: 'Alice' })
|
||||
expect(readIdentity(storage)).toEqual({ name: 'Alice' })
|
||||
})
|
||||
const storage = createMockStorage();
|
||||
writeIdentity(storage, { name: 'Alice' });
|
||||
expect(readIdentity(storage)).toEqual({ name: 'Alice' });
|
||||
});
|
||||
|
||||
it('ignores malformed JSON', () => {
|
||||
const storage = createMockStorage()
|
||||
storage.setItem('stb:identity', '{not json')
|
||||
expect(readIdentity(storage)).toBeNull()
|
||||
})
|
||||
const storage = createMockStorage();
|
||||
storage.setItem('stb:identity', '{not json');
|
||||
expect(readIdentity(storage)).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores an empty stored name', () => {
|
||||
const storage = createMockStorage()
|
||||
storage.setItem('stb:identity', JSON.stringify({ name: ' ' }))
|
||||
expect(readIdentity(storage)).toBeNull()
|
||||
})
|
||||
const storage = createMockStorage();
|
||||
storage.setItem('stb:identity', JSON.stringify({ name: ' ' }));
|
||||
expect(readIdentity(storage)).toBeNull();
|
||||
});
|
||||
|
||||
it('overwrites a previously stored identity', () => {
|
||||
const storage = createMockStorage()
|
||||
writeIdentity(storage, { name: 'Alice' })
|
||||
writeIdentity(storage, { name: 'Bob' })
|
||||
expect(readIdentity(storage)).toEqual({ name: 'Bob' })
|
||||
})
|
||||
})
|
||||
const storage = createMockStorage();
|
||||
writeIdentity(storage, { name: 'Alice' });
|
||||
writeIdentity(storage, { name: 'Bob' });
|
||||
expect(readIdentity(storage)).toEqual({ name: 'Bob' });
|
||||
});
|
||||
});
|
||||
|
||||
+11
-12
@@ -1,26 +1,25 @@
|
||||
const STORAGE_KEY = 'stb:identity'
|
||||
const STORAGE_KEY = 'stb:identity';
|
||||
|
||||
export interface Identity {
|
||||
name: string
|
||||
name: string;
|
||||
}
|
||||
|
||||
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>
|
||||
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>;
|
||||
|
||||
export function readIdentity(storage: StorageLike): Identity | null {
|
||||
const raw = storage.getItem(STORAGE_KEY)
|
||||
if (!raw) return null
|
||||
const raw = storage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (typeof parsed?.name !== 'string' || !parsed.name.trim()) return null
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed?.name !== 'string' || !parsed.name.trim()) return null;
|
||||
|
||||
return { name: parsed.name }
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
return { name: parsed.name };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeIdentity(storage: StorageLike, identity: Identity): void {
|
||||
storage.setItem(STORAGE_KEY, JSON.stringify(identity))
|
||||
storage.setItem(STORAGE_KEY, JSON.stringify(identity));
|
||||
}
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readCachedPaymentLink, writeCachedPaymentLink } from './paymentLinkCache'
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
readCachedPaymentLink,
|
||||
writeCachedPaymentLink,
|
||||
} from './paymentLinkCache';
|
||||
|
||||
function createMockStorage(): Storage {
|
||||
const store = new Map<string, string>()
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void store.set(key, value),
|
||||
removeItem: (key: string) => void store.delete(key),
|
||||
clear: () => store.clear(),
|
||||
key: () => null,
|
||||
get length() { return store.size },
|
||||
}
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('payment link cache', () => {
|
||||
it('returns null when nothing is cached', () => {
|
||||
expect(readCachedPaymentLink(createMockStorage())).toBeNull()
|
||||
})
|
||||
expect(readCachedPaymentLink(createMockStorage())).toBeNull();
|
||||
});
|
||||
|
||||
it('round-trips a cached link', () => {
|
||||
const storage = createMockStorage()
|
||||
writeCachedPaymentLink(storage, 'https://paypal.me/alice')
|
||||
expect(readCachedPaymentLink(storage)).toBe('https://paypal.me/alice')
|
||||
})
|
||||
const storage = createMockStorage();
|
||||
writeCachedPaymentLink(storage, 'https://paypal.me/alice');
|
||||
expect(readCachedPaymentLink(storage)).toBe('https://paypal.me/alice');
|
||||
});
|
||||
|
||||
it('overwrites a previously cached link', () => {
|
||||
const storage = createMockStorage()
|
||||
writeCachedPaymentLink(storage, 'https://paypal.me/alice')
|
||||
writeCachedPaymentLink(storage, 'https://paypal.me/bob')
|
||||
expect(readCachedPaymentLink(storage)).toBe('https://paypal.me/bob')
|
||||
})
|
||||
})
|
||||
const storage = createMockStorage();
|
||||
writeCachedPaymentLink(storage, 'https://paypal.me/alice');
|
||||
writeCachedPaymentLink(storage, 'https://paypal.me/bob');
|
||||
expect(readCachedPaymentLink(storage)).toBe('https://paypal.me/bob');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const STORAGE_KEY = 'stb:last-payment-link'
|
||||
const STORAGE_KEY = 'stb:last-payment-link';
|
||||
|
||||
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>
|
||||
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>;
|
||||
|
||||
export function readCachedPaymentLink(storage: StorageLike): string | null {
|
||||
return storage.getItem(STORAGE_KEY) || null
|
||||
return storage.getItem(STORAGE_KEY) || null;
|
||||
}
|
||||
|
||||
export function writeCachedPaymentLink(storage: StorageLike, link: string): void {
|
||||
storage.setItem(STORAGE_KEY, link)
|
||||
export function writeCachedPaymentLink(
|
||||
storage: StorageLike,
|
||||
link: string,
|
||||
): void {
|
||||
storage.setItem(STORAGE_KEY, link);
|
||||
}
|
||||
|
||||
@@ -1,67 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readVisitedPots, recordVisitedPot } from './visitedPots'
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readVisitedPots, recordVisitedPot } from './visitedPots';
|
||||
|
||||
function createMockStorage(): Storage {
|
||||
const store = new Map<string, string>()
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void store.set(key, value),
|
||||
removeItem: (key: string) => void store.delete(key),
|
||||
clear: () => store.clear(),
|
||||
key: () => null,
|
||||
get length() { return store.size },
|
||||
}
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('visited pots storage', () => {
|
||||
it('returns an empty list when nothing is stored', () => {
|
||||
expect(readVisitedPots(createMockStorage())).toEqual([])
|
||||
})
|
||||
expect(readVisitedPots(createMockStorage())).toEqual([]);
|
||||
});
|
||||
|
||||
it('records a visited pot', () => {
|
||||
const storage = createMockStorage()
|
||||
recordVisitedPot(storage, { slug: 'abc', name: 'Lisbon Trip', currency: 'EUR' })
|
||||
const storage = createMockStorage();
|
||||
recordVisitedPot(storage, {
|
||||
slug: 'abc',
|
||||
name: 'Lisbon Trip',
|
||||
currency: 'EUR',
|
||||
});
|
||||
expect(readVisitedPots(storage)).toEqual([
|
||||
expect.objectContaining({ slug: 'abc', name: 'Lisbon Trip', currency: 'EUR' }),
|
||||
])
|
||||
})
|
||||
expect.objectContaining({
|
||||
slug: 'abc',
|
||||
name: 'Lisbon Trip',
|
||||
currency: 'EUR',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('moves a re-visited pot to the front instead of duplicating it', () => {
|
||||
const storage = createMockStorage()
|
||||
recordVisitedPot(storage, { slug: 'abc', name: 'Lisbon Trip', currency: 'EUR' })
|
||||
recordVisitedPot(storage, { slug: 'def', name: 'Ski Weekend', currency: 'CHF' })
|
||||
recordVisitedPot(storage, { slug: 'abc', name: 'Lisbon Trip', currency: 'EUR' })
|
||||
const storage = createMockStorage();
|
||||
recordVisitedPot(storage, {
|
||||
slug: 'abc',
|
||||
name: 'Lisbon Trip',
|
||||
currency: 'EUR',
|
||||
});
|
||||
recordVisitedPot(storage, {
|
||||
slug: 'def',
|
||||
name: 'Ski Weekend',
|
||||
currency: 'CHF',
|
||||
});
|
||||
recordVisitedPot(storage, {
|
||||
slug: 'abc',
|
||||
name: 'Lisbon Trip',
|
||||
currency: 'EUR',
|
||||
});
|
||||
|
||||
const pots = readVisitedPots(storage)
|
||||
expect(pots).toHaveLength(2)
|
||||
expect(pots[0]?.slug).toBe('abc')
|
||||
})
|
||||
const pots = readVisitedPots(storage);
|
||||
expect(pots).toHaveLength(2);
|
||||
expect(pots[0]?.slug).toBe('abc');
|
||||
});
|
||||
|
||||
it('updates the stored name and currency on revisit', () => {
|
||||
const storage = createMockStorage()
|
||||
recordVisitedPot(storage, { slug: 'abc', name: 'Old Name', currency: 'EUR' })
|
||||
recordVisitedPot(storage, { slug: 'abc', name: 'New Name', currency: 'USD' })
|
||||
const storage = createMockStorage();
|
||||
recordVisitedPot(storage, {
|
||||
slug: 'abc',
|
||||
name: 'Old Name',
|
||||
currency: 'EUR',
|
||||
});
|
||||
recordVisitedPot(storage, {
|
||||
slug: 'abc',
|
||||
name: 'New Name',
|
||||
currency: 'USD',
|
||||
});
|
||||
|
||||
expect(readVisitedPots(storage)).toEqual([
|
||||
expect.objectContaining({ slug: 'abc', name: 'New Name', currency: 'USD' }),
|
||||
])
|
||||
})
|
||||
expect.objectContaining({
|
||||
slug: 'abc',
|
||||
name: 'New Name',
|
||||
currency: 'USD',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores malformed JSON', () => {
|
||||
const storage = createMockStorage()
|
||||
storage.setItem('stb:visited-pots', '{not json')
|
||||
expect(readVisitedPots(storage)).toEqual([])
|
||||
})
|
||||
const storage = createMockStorage();
|
||||
storage.setItem('stb:visited-pots', '{not json');
|
||||
expect(readVisitedPots(storage)).toEqual([]);
|
||||
});
|
||||
|
||||
it('caps the list at 20 entries, keeping the most recent', () => {
|
||||
const storage = createMockStorage()
|
||||
const storage = createMockStorage();
|
||||
for (let i = 0; i < 25; i++) {
|
||||
recordVisitedPot(storage, { slug: `pot-${i}`, name: `Pot ${i}`, currency: 'EUR' })
|
||||
recordVisitedPot(storage, {
|
||||
slug: `pot-${i}`,
|
||||
name: `Pot ${i}`,
|
||||
currency: 'EUR',
|
||||
});
|
||||
}
|
||||
|
||||
const pots = readVisitedPots(storage)
|
||||
expect(pots).toHaveLength(20)
|
||||
expect(pots[0]?.slug).toBe('pot-24')
|
||||
expect(pots.some(p => p.slug === 'pot-0')).toBe(false)
|
||||
})
|
||||
})
|
||||
const pots = readVisitedPots(storage);
|
||||
expect(pots).toHaveLength(20);
|
||||
expect(pots[0]?.slug).toBe('pot-24');
|
||||
expect(pots.some((p) => p.slug === 'pot-0')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+29
-25
@@ -1,40 +1,44 @@
|
||||
const STORAGE_KEY = 'stb:visited-pots'
|
||||
const MAX_ENTRIES = 20
|
||||
const STORAGE_KEY = 'stb:visited-pots';
|
||||
const MAX_ENTRIES = 20;
|
||||
|
||||
export interface VisitedPot {
|
||||
slug: string
|
||||
name: string
|
||||
currency: string
|
||||
visitedAt: number
|
||||
slug: string;
|
||||
name: string;
|
||||
currency: string;
|
||||
visitedAt: number;
|
||||
}
|
||||
|
||||
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>
|
||||
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>;
|
||||
|
||||
export function readVisitedPots(storage: StorageLike): VisitedPot[] {
|
||||
const raw = storage.getItem(STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
const raw = storage.getItem(STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed.filter((item): item is VisitedPot =>
|
||||
typeof item?.slug === 'string' && item.slug.trim() !== ''
|
||||
&& typeof item?.name === 'string'
|
||||
&& typeof item?.currency === 'string'
|
||||
&& typeof item?.visitedAt === 'number',
|
||||
)
|
||||
}
|
||||
catch {
|
||||
return []
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter(
|
||||
(item): item is VisitedPot =>
|
||||
typeof item?.slug === 'string' &&
|
||||
item.slug.trim() !== '' &&
|
||||
typeof item?.name === 'string' &&
|
||||
typeof item?.currency === 'string' &&
|
||||
typeof item?.visitedAt === 'number',
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function recordVisitedPot(storage: StorageLike, pot: Omit<VisitedPot, 'visitedAt'>): VisitedPot[] {
|
||||
const rest = readVisitedPots(storage).filter(p => p.slug !== pot.slug)
|
||||
export function recordVisitedPot(
|
||||
storage: StorageLike,
|
||||
pot: Omit<VisitedPot, 'visitedAt'>,
|
||||
): VisitedPot[] {
|
||||
const rest = readVisitedPots(storage).filter((p) => p.slug !== pot.slug);
|
||||
const updated = [{ ...pot, visitedAt: Date.now() }, ...rest]
|
||||
.sort((a, b) => b.visitedAt - a.visitedAt)
|
||||
.slice(0, MAX_ENTRIES)
|
||||
.slice(0, MAX_ENTRIES);
|
||||
|
||||
storage.setItem(STORAGE_KEY, JSON.stringify(updated))
|
||||
return updated
|
||||
storage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
||||
return updated;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user