Files
readest/apps/readest-app/src/__tests__/hooks/useBackgroundTexture.test.tsx
T
Huang Xin 7d1a60b9ea feat(library): separate background texture for library and reader (#4754)
* feat(library): separate background texture for library and reader (#4743)

The library and reader shared a single background texture, so a reader
backdrop with borders or other reading-oriented decoration looked wrong
on the bookshelf. Let users set them independently.

- Add device-local libraryBackground{TextureId,Opacity,Size} to
  SystemSettings. Each field falls back to the reader/global value when
  unset (getLibraryViewSettings), so an existing bookshelf looks
  unchanged until the user picks a library texture, then decouples
  per-field. No migration needed; the selection stays per-device like
  the reader's, while imported images keep syncing via the texture kind.
- Make the Color panel's Background Image picker context-aware: opened
  from the library it edits the library texture, opened while reading it
  edits the reader texture. A sublabel states which page it applies to.
- Apply the library texture at boot and on every library mount, so
  returning from a textured book restores the bookshelf background.
- useBackgroundTexture now unmounts on 'none' instead of early-returning,
  since library and reader share one style element: switching a page to
  None must clear a texture the other page mounted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n: translate library/reader background texture labels (#4743)

Add translations for the two new context sublabels ("Applies to the
Library" / "Applies to the Reader") across all 33 locales, anchored to
each locale's existing Library and reading terminology.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 07:24:30 +02:00

132 lines
5.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { cleanup, renderHook } from '@testing-library/react';
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { useSettingsStore } from '@/store/settingsStore';
import { CustomTexture } from '@/styles/textures';
import { SystemSettings } from '@/types/settings';
import { ViewSettings } from '@/types/book';
import { EnvConfigType } from '@/services/environment';
vi.mock('@/styles/textures', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/styles/textures')>();
return {
...actual,
mountBackgroundTexture: vi.fn(),
unmountBackgroundTexture: vi.fn(),
};
});
const createMockEnvConfig = (): EnvConfigType =>
({
getAppService: vi.fn().mockResolvedValue({
openFile: vi.fn().mockResolvedValue({
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
}),
}),
}) as unknown as EnvConfigType;
// Use a saved id that does NOT equal getTextureId(name). This mirrors the
// real-world bug: legacy/synced textures persist an id that wasn't derived
// from the current name. addTexture would re-derive the id from name and
// create a SECOND store entry with the wrong id, leaving the real id
// unfindable. The fix path therefore must seed the store with the saved id.
const SAVED_TEXTURE: CustomTexture = {
id: 'legacy-saved-id',
name: 'my-cool-texture',
path: 'legacy-saved-id/my-cool-texture.png',
};
const makeViewSettings = (overrides: Partial<ViewSettings> = {}): ViewSettings =>
({
backgroundTextureId: SAVED_TEXTURE.id,
backgroundOpacity: 0.6,
backgroundSize: 'cover',
...overrides,
}) as unknown as ViewSettings;
beforeEach(() => {
useCustomTextureStore.setState({ textures: [], loading: false });
useSettingsStore.setState({ settings: {} as SystemSettings });
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe('useBackgroundTexture', () => {
test('mounts a custom texture when the store is pre-seeded with its saved id', async () => {
// Simulates the fixed boot path: Providers seeds the customTextureStore
// via setTextures(settings.customTextures) before calling the hook. The
// hook's applyTexture then finds the texture by its saved id.
const { mountBackgroundTexture } = await import('@/styles/textures');
vi.mocked(mountBackgroundTexture).mockClear();
useCustomTextureStore.getState().setTextures([SAVED_TEXTURE]);
const { result } = renderHook(() => useBackgroundTexture());
result.current.applyBackgroundTexture(createMockEnvConfig(), makeViewSettings());
await vi.waitFor(() => {
expect(mountBackgroundTexture).toHaveBeenCalledTimes(1);
});
const mountedTexture = vi.mocked(mountBackgroundTexture).mock.calls[0]?.[1];
expect(mountedTexture?.id).toBe(SAVED_TEXTURE.id);
});
test('does NOT mount the texture when the store is empty (documents the boot bug without the seeding fix)', async () => {
// Without setTextures seeding, the in-hook addTexture re-derives an id
// from name and adds a different entry, so applyTexture never finds the
// saved id and unmounts instead of mounting.
const { mountBackgroundTexture, unmountBackgroundTexture } = await import('@/styles/textures');
vi.mocked(mountBackgroundTexture).mockClear();
vi.mocked(unmountBackgroundTexture).mockClear();
useSettingsStore.setState({
settings: { customTextures: [SAVED_TEXTURE] } as unknown as SystemSettings,
});
const { result } = renderHook(() => useBackgroundTexture());
result.current.applyBackgroundTexture(createMockEnvConfig(), makeViewSettings());
await vi.waitFor(() => {
expect(unmountBackgroundTexture).toHaveBeenCalled();
});
expect(mountBackgroundTexture).not.toHaveBeenCalled();
});
test('does nothing when textureId is "none"', () => {
const { result } = renderHook(() => useBackgroundTexture());
result.current.applyBackgroundTexture(
createMockEnvConfig(),
makeViewSettings({ backgroundTextureId: 'none' }),
);
expect(useCustomTextureStore.getState().getTexture(SAVED_TEXTURE.id)).toBeUndefined();
});
test('unmounts any mounted texture when textureId is "none"', async () => {
// Library/reader share the single #background-texture style element. When a
// page whose texture is "none" becomes active (e.g. returning to a
// None-background library after reading a textured book), applying it must
// actively clear the leftover texture, not silently leave it mounted.
const { mountBackgroundTexture, unmountBackgroundTexture } = await import('@/styles/textures');
vi.mocked(mountBackgroundTexture).mockClear();
vi.mocked(unmountBackgroundTexture).mockClear();
const { result } = renderHook(() => useBackgroundTexture());
result.current.applyBackgroundTexture(
createMockEnvConfig(),
makeViewSettings({ backgroundTextureId: 'none' }),
);
await vi.waitFor(() => {
expect(unmountBackgroundTexture).toHaveBeenCalled();
});
expect(mountBackgroundTexture).not.toHaveBeenCalled();
});
});