Build and Push Docker Image / build-and-push (push) Failing after 19s
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>
73 lines
3.4 KiB
JavaScript
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();
|
|
});
|