diff --git a/web/src/settings/AISettingsPage.test.tsx b/web/src/settings/AISettingsPage.test.tsx index 903fabd..1ad7dbd 100644 --- a/web/src/settings/AISettingsPage.test.tsx +++ b/web/src/settings/AISettingsPage.test.tsx @@ -657,6 +657,29 @@ describe('Recipe search card', () => { expect(await screen.findByRole('dialog', { name: 'Leave without saving?' })).toBeInTheDocument() }) + it('does not count the suggested SearXNG URL as an unsaved change until it is edited', async () => { + stubAPI({ 'GET /api/v1/settings/recipe-search': () => jsonResponse(200, UNCONFIGURED) }) + await renderCard() + const unload = () => { + const event = new Event('beforeunload', { cancelable: true }) + act(() => void window.dispatchEvent(event)) + return event.defaultPrevented + } + expect(saveSearch()).toBeEnabled() + expect(unload()).toBe(false) + fireEvent.change(url(), { target: { value: 'https://search.example' } }) + expect(unload()).toBe(true) + fireEvent.change(url(), { target: { value: 'http://searxng:8080' } }) + expect(unload()).toBe(false) + }) + + it('leaves the untouched first-use card without asking', async () => { + stubAPI({ 'GET /api/v1/settings/recipe-search': () => jsonResponse(200, UNCONFIGURED) }) + await renderCard() + fireEvent.click(screen.getByRole('button', { name: 'Back to Settings' })) + expect(await screen.findByTestId('path')).toHaveTextContent('/settings') + }) + it('keeps 44 px tap targets', async () => { stubAPI({ 'GET /api/v1/settings/recipe-search': () => jsonResponse(200, BRAVE_SETTINGS) }) await renderCard() diff --git a/web/src/settings/RecipeSearchCard.tsx b/web/src/settings/RecipeSearchCard.tsx index 9d54064..7cf7daf 100644 --- a/web/src/settings/RecipeSearchCard.tsx +++ b/web/src/settings/RecipeSearchCard.tsx @@ -81,16 +81,24 @@ function patchOf(saved: RecipeSearchSettings, draft: Draft): RecipeSearchSetting return patch } +function sameDraft(a: Draft, b: Draft): boolean { + return a.provider === b.provider && a.base_url === b.base_url && a.api_key === b.api_key +} + function RecipeSearchForm({ saved, onDirtyChange }: { saved: RecipeSearchSettings } & RecipeSearchCardProps) { const [draft, setDraft] = useState(() => draftOf(saved)) const save = useSaveRecipeSearchSettings() const test = useTestRecipeSearch() const patch = patchOf(saved, draft) const dirty = Object.keys(patch).length > 0 + // Only the household's own edits are unsaved changes. Before recipe search + // is set up the untouched draft already differs from what is saved — it + // suggests SearXNG and the deployment's URL — but leaving it loses nothing. + const edited = dirty && !sameDraft(draft, draftOf(saved)) useEffect(() => { - onDirtyChange(dirty) - }, [dirty, onDirtyChange]) + onDirtyChange(edited) + }, [edited, onDirtyChange]) useEffect(() => () => onDirtyChange(false), [onDirtyChange]) // An edit clears the last save and test results: they describe the saved