fix(settings): persist complete display preferences

Accept the author display fields already emitted by the client and include them in automatic account sync. Keep older payloads compatible with defaults and cover the client-server contract with regression tests.

Fixes #1209
This commit is contained in:
neon
2026-08-30 05:59:19 +00:00
parent 9f15ced2cb
commit 3196454944
6 changed files with 79 additions and 1 deletions
@@ -51,11 +51,15 @@ const validDisplayPreferences: DisplayPreferences = {
smartScopeFilterExpanded: true,
authorCoverSize: 140,
authorCoverShape: 'circle',
authorRowDensity: 'comfortable',
authorCoverFallback: false,
tableZebraStriping: false,
tableDensity: 'comfortable',
bookSpineOverlay: 'subtle',
showSpineOnComics: false,
bookShadowStrength: 'strong',
bookCoverDisplayMode: 'natural-bottom',
bookDetailCoverTint: 'duotone',
seriesCardCoverMode: 'stack',
gridCardPrimaryLabel: 'hidden',
gridCardSecondaryLabel: 'hidden',
@@ -71,6 +71,8 @@ const validDisplayPreferences: DisplayPreferences = {
smartScopeFilterExpanded: true,
authorCoverSize: 140,
authorCoverShape: 'circle',
authorRowDensity: 'comfortable',
authorCoverFallback: false,
tableZebraStriping: false,
tableDensity: 'comfortable',
bookSpineOverlay: 'subtle',
@@ -404,6 +406,36 @@ describe('UserPreferencesService', () => {
expect(repo.upsert).not.toHaveBeenCalled();
});
it('upsertDisplayPreferences accepts and persists author display preferences', async () => {
const preferences = {
...validDisplayPreferences,
authorRowDensity: 'compact',
authorCoverFallback: true,
} satisfies DisplayPreferences;
await expect(service.upsertDisplayPreferences(11, preferences)).resolves.toBeUndefined();
expect(repo.upsert).toHaveBeenCalledWith(11, 'display', preferences);
});
it('upsertDisplayPreferences defaults author display preferences omitted by older clients', async () => {
const { authorRowDensity, authorCoverFallback, ...olderPreferences } = validDisplayPreferences;
void authorRowDensity;
void authorCoverFallback;
await expect(service.upsertDisplayPreferences(11, olderPreferences)).resolves.toBeUndefined();
expect(repo.upsert).toHaveBeenCalledWith(11, 'display', expect.objectContaining({ authorRowDensity: 'comfortable', authorCoverFallback: false }));
});
it.each([
['authorRowDensity', 'dense'],
['authorCoverFallback', 'yes'],
])('upsertDisplayPreferences rejects invalid %s values', async (field, value) => {
await expect(service.upsertDisplayPreferences(11, { ...validDisplayPreferences, [field]: value })).rejects.toBeInstanceOf(BadRequestException);
expect(repo.upsert).not.toHaveBeenCalled();
});
it('upsertDisplayPreferences rejects invalid cover display modes', async () => {
await expect(
service.upsertDisplayPreferences(11, { ...validDisplayPreferences, bookCoverDisplayMode: 'stretched' } as never),
@@ -61,6 +61,8 @@ const DISPLAY_PREFERENCES_SCHEMA = z
smartScopeFilterExpanded: z.boolean(),
authorCoverSize: z.number().int().min(100).max(400),
authorCoverShape: z.enum(AUTHOR_COVER_SHAPES),
authorRowDensity: z.enum(TABLE_DENSITIES).default('comfortable'),
authorCoverFallback: z.boolean().default(false),
tableZebraStriping: z.boolean(),
tableDensity: z.enum(TABLE_DENSITIES),
bookSpineOverlay: z.enum(BOOK_SPINE_OVERLAYS),
@@ -83,7 +85,7 @@ const DISPLAY_PREFERENCES_SCHEMA = z
message: 'cardOverlays must not contain duplicate values',
});
}
});
}) satisfies z.ZodType<DisplayPreferences>;
const COVER_SEARCH_PREFERENCES_SCHEMA = z
.object({