diff --git a/src/apps/settings/WallpaperSection.test.tsx b/src/apps/settings/WallpaperSection.test.tsx index 7f540af..5ed0afc 100644 --- a/src/apps/settings/WallpaperSection.test.tsx +++ b/src/apps/settings/WallpaperSection.test.tsx @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { TestApp } from '@/test/TestApp'; import { WallpaperSection } from './index'; @@ -13,9 +13,45 @@ class StubResizeObserver { disconnect() {} } +// A controllable stand-in for the browser's `Image` constructor, mirroring +// the one in useDecodedImage.test.ts, so a preview can be resolved on demand. +class FakeImage { + decoding = ''; + src = ''; + naturalWidth = 40; + naturalHeight = 30; + private resolveDecode!: () => void; + readonly decodePromise = new Promise((resolve) => { + this.resolveDecode = resolve; + }); + + constructor() { + instances.push(this); + } + + decode() { + return this.decodePromise; + } + + finish() { + this.resolveDecode(); + } +} + +let instances: FakeImage[] = []; + describe('WallpaperSection', () => { + let originalImage: typeof Image; + beforeEach(() => { global.ResizeObserver = StubResizeObserver as unknown as typeof ResizeObserver; + instances = []; + originalImage = globalThis.Image; + globalThis.Image = FakeImage as unknown as typeof Image; + }); + + afterEach(() => { + globalThis.Image = originalImage; }); it('shows the dot-grid curated wallpaper selected by default', async () => { @@ -52,4 +88,24 @@ describe('WallpaperSection', () => { expect(saveButton).toBeDisabled(); expect(screen.queryByAltText('Wallpaper preview')).not.toBeInTheDocument(); }); + + it('previews and saves a valid https custom URL, switching off the curated selection', async () => { + render(, { wrapper: TestApp }); + + const urlInput = await screen.findByLabelText('Custom image'); + fireEvent.change(urlInput, { target: { value: 'https://example.com/wallpaper.jpg' } }); + fireEvent.click(screen.getByRole('button', { name: 'Preview' })); + + await waitFor(() => expect(instances).toHaveLength(1)); + instances[0].finish(); + + const previewImg = await screen.findByAltText('Wallpaper preview'); + expect(previewImg).toHaveAttribute('src', 'https://example.com/wallpaper.jpg'); + + const saveButton = screen.getByRole('button', { name: 'Save wallpaper' }); + await waitFor(() => expect(saveButton).not.toBeDisabled()); + fireEvent.click(saveButton); + + await waitFor(() => expect(screen.getByRole('radio', { name: 'Dot grid' })).not.toBeChecked()); + }); }); diff --git a/src/apps/settings/index.tsx b/src/apps/settings/index.tsx index fc0f9d1..1b5b643 100644 --- a/src/apps/settings/index.tsx +++ b/src/apps/settings/index.tsx @@ -179,6 +179,22 @@ export function WallpaperSection() { const [pendingPreviewUrl, setPendingPreviewUrl] = useState(undefined); const preview = useDecodedImage(pendingPreviewUrl); + // Keep the draft/presentation fields in sync if the saved selection changes + // from elsewhere (e.g. the desktop context menu's curated shortcut, or + // another open Settings window), so this form never shows a stale value. + // Adjusted during render (mirroring useLocalStorage's "key changed" reset + // pattern) rather than in a useEffect, since setState synchronously at the + // top of an effect body is flagged by react-hooks/set-state-in-effect. + const [trackedSelection, setTrackedSelection] = useState(selection); + if (trackedSelection !== selection) { + setTrackedSelection(selection); + setUrlDraft(selection.source === 'url' ? selection.url : ''); + setFit(selection.source === 'url' ? selection.presentation.fit : 'cover'); + setDim(selection.source === 'url' ? selection.presentation.dim : 0); + setPendingPreviewUrl(undefined); + } + + const applyCurated = (id: string) => { updateConfig((current) => ({ ...current, wallpaper: { version: 1, selection: { source: 'curated', id } } })); setPendingPreviewUrl(undefined); @@ -269,7 +285,7 @@ export function WallpaperSection() { description="Personalize the desktop background. Curated patterns recolor with your theme and never leave the device; a custom image is saved only in this browser." >