Sync wallpaper form with external selection changes; add custom-URL success test

Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-06 21:31:18 +00:00
committed by GitHub
parent 7600e8e5bf
commit 89dd31bb58
2 changed files with 74 additions and 2 deletions

View File

@@ -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<void>((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(<WallpaperSection />, { 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());
});
});

View File

@@ -179,6 +179,22 @@ export function WallpaperSection() {
const [pendingPreviewUrl, setPendingPreviewUrl] = useState<string | undefined>(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."
>
<RadioGroup
value={selection.source === 'curated' ? selection.id : undefined}
value={selection.source === 'curated' ? selection.id : ''}
onValueChange={applyCurated}
aria-label="Curated wallpapers"
className="grid grid-cols-2 gap-3 sm:grid-cols-3"