Files
study-bible-project/server/readerMarks.test.js
T
nmemmertandClaude Sonnet 5.5 a0971eab29
Build and Push Docker Image / build-and-push (push) Failing after 19s
Reader: visible text selection in every theme; highlights follow across translations
Selection
- Selected text uses a per-theme colour (it was nearly invisible on Dark)
- The colour picker also appears on touch screens after dragging the selection
  handles, and sits below the selection there so the system menu doesn't cover it

Highlights
- Each highlight remembers its translation and whether it covers the whole verse
- In another translation the same words are found again (ignoring case and
  punctuation); whole-verse highlights cover the whole verse; when the wording
  differs, the verse gets a small colour dot instead of marking the wrong words
- Older highlights are checked against their saved words instead of trusted
- Server keeps the new fields

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 09:55:09 -04:00

73 lines
3.4 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 { cleanMarks } from './readerMarks.js';
const hl = (over = {}) => ({ id: 'h1', book: 'JHN', chapter: 3, verse: 16, startOffset: 4, endOffset: 20, color: '#fef08a', ...over });
test('a non-object body is rejected', () => {
expect(cleanMarks(null)).toBeNull();
expect(cleanMarks([])).toBeNull();
expect(cleanMarks('x')).toBeNull();
});
test('good bookmarks and highlights pass through unchanged', () => {
const body = { bookmarks: { 'JHN-3-16': '#fef08a', '1SA-3-5': '#bbf7d0' }, highlights: [hl()] };
expect(cleanMarks(body)).toEqual(body);
});
test('malformed bookmarks are dropped, good ones kept', () => {
const out = cleanMarks({ bookmarks: { 'JHN-3-16': '#fef08a', 'bad key': '#fef08a', 'GEN-1-1': 'red', 'GEN-1-2': 5, '../x-1-1': '#fff' } });
expect(out.bookmarks).toEqual({ 'JHN-3-16': '#fef08a' });
});
test('highlights are validated field by field and de-duplicated by id', () => {
const out = cleanMarks({ highlights: [
hl(),
hl({ id: 'h1', verse: 17 }), // duplicate id
hl({ id: '' }), hl({ id: 'x'.repeat(65) }),
hl({ id: 'a', book: 'JOHN' }), hl({ id: 'b', chapter: 0 }), hl({ id: 'c', verse: 201 }),
hl({ id: 'd', startOffset: 10, endOffset: 10 }), hl({ id: 'e', startOffset: -1 }), hl({ id: 'f', color: 'yellow' }),
'nope', null,
hl({ id: 'ok', book: 'gen' }),
] });
expect(out.highlights.map((h) => h.id)).toEqual(['h1', 'ok']);
expect(out.highlights[1].book).toBe('GEN'); // normalised
});
test('missing sections become empty ones, and unknown fields are not stored', () => {
expect(cleanMarks({})).toEqual({ bookmarks: {}, highlights: [] });
expect(cleanMarks({ highlights: [{ ...hl(), evil: '<script>' }] }).highlights[0]).not.toHaveProperty('evil');
});
test('the database keeps one row per user and updatedAt only moves forward', async () => {
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), 'marks-'));
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.getReaderMarks('u1')).toEqual({ bookmarks: {}, highlights: [], updatedAt: 0 });
const t1 = db.setReaderMarks('u1', { bookmarks: { 'JHN-3-16': '#fef08a' }, highlights: [] });
const t2 = db.setReaderMarks('u1', { bookmarks: {}, highlights: [hl()] });
expect(t2).toBeGreaterThan(t1);
const saved = db.getReaderMarks('u1');
expect(saved.updatedAt).toBe(t2);
expect(saved.bookmarks).toEqual({});
expect(saved.highlights).toHaveLength(1);
expect(db.getReaderMarks('u2').updatedAt).toBe(0); // other users are untouched
});
test('a highlight keeps the translation it was made in and whether it covers the whole verse', async () => {
const { cleanMarks } = await import('./readerMarks.js');
const base = { id: 'a', book: 'JHN', chapter: 3, verse: 16, startOffset: 0, endOffset: 40, color: '#fef08a', text: 'For God so loved' };
const out = cleanMarks({ highlights: [
{ ...base, translation: 'BSB', whole: true },
{ ...base, id: 'b', translation: 'not a code!!', whole: 'yes' },
] }).highlights;
expect(out[0]).toMatchObject({ translation: 'BSB', whole: true });
expect(out[1].translation).toBeUndefined();
expect(out[1].whole).toBeUndefined();
});