Build and Push Docker Image / build-and-push (push) Failing after 19s
Bookmarks and text highlights are saved per user and merged across devices (union on first contact, newer server copy when this device is clean). Theme, default translation, verse layout, commentary source and reading position sync too, each with its own timestamp so the newest change to each setting wins. The Reader never moves under you while it is open. Signing out clears this device's marks and forgets sync timestamps. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
65 lines
3.1 KiB
JavaScript
65 lines
3.1 KiB
JavaScript
// @vitest-environment node
|
|
import { mkdtempSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { test, expect } from 'vitest';
|
|
import { cleanPrefValue, cleanPrefsPatch, mergePrefs } from './prefs.js';
|
|
|
|
const NOW = 1_800_000_000_000;
|
|
|
|
test('only known preferences with valid values are accepted', () => {
|
|
expect(cleanPrefValue('theme', 'dark')).toBe('dark');
|
|
expect(cleanPrefValue('theme', 'neon')).toBeUndefined();
|
|
expect(cleanPrefValue('translation', 'ESV')).toBe('ESV');
|
|
expect(cleanPrefValue('translation', 'KJV')).toBeUndefined();
|
|
expect(cleanPrefValue('layout', 'flow')).toBe('flow');
|
|
expect(cleanPrefValue('commentarySource', 'tyndale')).toBe('tyndale');
|
|
expect(cleanPrefValue('fontSize', 2)).toBeUndefined(); // not a synced preference
|
|
});
|
|
|
|
test('position needs a real book and chapter, and the book is normalised', () => {
|
|
expect(cleanPrefValue('position', { book: 'jhn', chapter: 3 })).toEqual({ book: 'JHN', chapter: 3 });
|
|
expect(cleanPrefValue('position', { book: 'JOHN', chapter: 3 })).toBeUndefined();
|
|
expect(cleanPrefValue('position', { book: 'JHN', chapter: 0 })).toBeUndefined();
|
|
expect(cleanPrefValue('position', { book: 'JHN', chapter: '3' })).toBeUndefined();
|
|
expect(cleanPrefValue('position', null)).toBeUndefined();
|
|
});
|
|
|
|
test('cleanPrefsPatch drops unknown keys, bad values and silly timestamps', () => {
|
|
const patch = cleanPrefsPatch({ prefs: {
|
|
theme: { v: 'sepia', at: NOW },
|
|
layout: { v: 'nope', at: NOW },
|
|
evil: { v: 'x', at: NOW },
|
|
translation: { v: 'NLT', at: 'yesterday' },
|
|
commentarySource: { v: 'tyndale', at: NOW + 5 * 86_400_000 }, // far in the future
|
|
position: { v: { book: 'GEN', chapter: 2 }, at: NOW - 10 },
|
|
} }, NOW);
|
|
expect(patch).toEqual({ theme: { v: 'sepia', at: NOW }, position: { v: { book: 'GEN', chapter: 2 }, at: NOW - 10 } });
|
|
expect(cleanPrefsPatch(null)).toEqual({});
|
|
expect(cleanPrefsPatch({ prefs: [] })).toEqual({});
|
|
});
|
|
|
|
test('the newest change to each preference wins, independently', () => {
|
|
const existing = { theme: { v: 'dark', at: 100 }, layout: { v: 'flow', at: 500 } };
|
|
const patch = { theme: { v: 'paper', at: 200 }, layout: { v: 'verses', at: 300 }, translation: { v: 'ESV', at: 50 } };
|
|
expect(mergePrefs(existing, patch)).toEqual({
|
|
theme: { v: 'paper', at: 200 }, // patch is newer
|
|
layout: { v: 'flow', at: 500 }, // stored is newer
|
|
translation: { v: 'ESV', at: 50 }, // new key
|
|
});
|
|
expect(mergePrefs({ theme: { v: 'dark', at: 100 } }, { theme: { v: 'paper', at: 100 } }).theme.v).toBe('dark'); // tie keeps stored
|
|
});
|
|
|
|
test('the database keeps one row per user', async () => {
|
|
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), 'prefs-'));
|
|
const db = await import('./db.js');
|
|
db.initDb();
|
|
db.createUser({ id: 'u1', email: 'a@example.test', passwordHash: 'x' });
|
|
db.createUser({ id: 'u2', email: 'b@example.test', passwordHash: 'x' });
|
|
expect(db.getUserPrefs('u1')).toEqual({});
|
|
db.setUserPrefs('u1', { theme: { v: 'dark', at: 1 } });
|
|
db.setUserPrefs('u1', { theme: { v: 'sepia', at: 2 } });
|
|
expect(db.getUserPrefs('u1')).toEqual({ theme: { v: 'sepia', at: 2 } });
|
|
expect(db.getUserPrefs('u2')).toEqual({});
|
|
});
|