From b0d46e2d3d7940548346bc6bf8af6c4e6d3266d2 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:08:40 -0700 Subject: [PATCH] fix(settings): preserve multiline proxy bypass rules (#20957) --- ...etworkSettingsSection.interaction.test.tsx | 63 +++++++++++++++++++ .../AdvancedNetworkSettingsSection.test.ts | 21 +++++++ .../AdvancedNetworkSettingsSection.tsx | 9 +-- tests/e2e/network-proxy-bypass-rules.spec.ts | 41 ++++++++++++ 4 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx create mode 100644 tests/e2e/network-proxy-bypass-rules.spec.ts diff --git a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx new file mode 100644 index 00000000000..513c2ed4e4e --- /dev/null +++ b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx @@ -0,0 +1,63 @@ +// @vitest-environment happy-dom + +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { getDefaultSettings } from '../../../../shared/constants' +import { AdvancedNetworkSettingsSection } from './AdvancedNetworkSettingsSection' + +afterEach(() => cleanup()) + +describe('AdvancedNetworkSettingsSection bypass rules control', () => { + it('keeps newline input and canonicalizes it when focus leaves the textarea', async () => { + const updateSettings = vi.fn() + + const { container } = render( + + ) + + const configureButton = Array.from(container.querySelectorAll('button')).find((button) => + button.textContent?.includes('Configure proxy') + ) + expect(configureButton).not.toBeUndefined() + fireEvent.click(configureButton!) + + const textarea = container.querySelector( + '#settings-http-proxy-bypass-rules' + ) + expect(textarea).not.toBeNull() + + fireEvent.change(textarea!, { target: { value: 'localhost\n127.0.0.1\n*.internal.corp' } }) + fireEvent.blur(textarea!) + + expect(updateSettings).toHaveBeenCalledWith({ + httpProxyBypassRules: 'localhost;127.0.0.1;*.internal.corp' + }) + }) + + it('does not commit when Enter is pressed inside the textarea', () => { + const updateSettings = vi.fn() + const { container } = render( + + ) + fireEvent.click( + Array.from(container.querySelectorAll('button')).find((button) => + button.textContent?.includes('Configure proxy') + )! + ) + const textarea = container.querySelector( + '#settings-http-proxy-bypass-rules' + )! + + textarea.focus() + fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter' }) + + expect(document.activeElement).toBe(textarea) + expect(updateSettings).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts index 08a392fdecc..cdbe8d446f3 100644 --- a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts +++ b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts @@ -1,6 +1,10 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import { createElement } from 'react' import { describe, expect, it } from 'vitest' import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { getDefaultSettings } from '../../../../shared/constants' import { + AdvancedNetworkSettingsSection, createHttpProxyBypassRulesDraftState, createHttpProxyUrlDraftState, hasConfiguredNetworkProxy, @@ -11,6 +15,23 @@ import { } from './AdvancedNetworkSettingsSection' describe('AdvancedNetworkSettingsSection proxy drafts', () => { + it('renders bypass rules as a multiline textarea', () => { + const markup = renderToStaticMarkup( + createElement(AdvancedNetworkSettingsSection, { + settings: { + ...getDefaultSettings('/tmp'), + httpProxyBypassRules: 'localhost\n127.0.0.1\n*.internal.corp' + }, + updateSettings: () => undefined + }) + ) + + expect(markup).toMatch(/]*id="settings-http-proxy-bypass-rules"[^>]*>/) + expect(markup).toContain('localhost') + expect(markup).toContain('127.0.0.1') + expect(markup).toContain('*.internal.corp') + }) + it('keeps a committed proxy URL draft tied to the current persisted source', () => { const current = createHttpProxyUrlDraftState(undefined) diff --git a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx index 6597dd1e15b..34939b067f4 100644 --- a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx +++ b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx @@ -9,6 +9,7 @@ import { Button } from '../ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible' import { Input } from '../ui/input' import { Label } from '../ui/label' +import { Textarea } from '../ui/textarea' import { getAdvancedNetworkSearchEntries } from './advanced-network-search' import { SearchableSetting } from './SearchableSetting' import { matchesSettingsSearch, normalizeSettingsSearchQuery } from './settings-search' @@ -304,16 +305,11 @@ export function AdvancedNetworkSettingsSection({ 'Proxy Bypass Rules' )} - updateHttpProxyBypassRulesDraft(e.target.value)} onBlur={commitHttpProxyBypassRules} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.currentTarget.blur() - } - }} placeholder={translate( 'auto.components.settings.AdvancedNetworkSettingsSection.3e431564b5', 'localhost, 127.0.0.1, *.internal' @@ -322,6 +318,7 @@ export function AdvancedNetworkSettingsSection({ autoCorrect="off" autoComplete="off" spellCheck={false} + rows={3} className="font-mono text-xs" /> diff --git a/tests/e2e/network-proxy-bypass-rules.spec.ts b/tests/e2e/network-proxy-bypass-rules.spec.ts new file mode 100644 index 00000000000..9db71f2fe60 --- /dev/null +++ b/tests/e2e/network-proxy-bypass-rules.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from './helpers/orca-app' +import { waitForSessionReady } from './helpers/store' + +test.describe('network proxy bypass rules', () => { + test('preserves newline-separated hosts and canonicalizes them on blur', async ({ orcaPage }) => { + await waitForSessionReady(orcaPage) + + const original = await orcaPage.evaluate(() => window.api.settings.get()) + try { + await orcaPage.evaluate(() => { + const state = window.__store?.getState() + state?.openSettingsTarget({ pane: 'advanced', repoId: null }) + state?.openSettingsPage() + }) + + await expect(orcaPage.getByRole('heading', { name: 'Advanced', exact: true })).toBeVisible() + await orcaPage.getByRole('button', { name: 'Configure proxy' }).click() + const bypassRules = orcaPage.locator('#settings-http-proxy-bypass-rules') + await expect(bypassRules).toBeVisible() + await expect(bypassRules).toHaveJSProperty('tagName', 'TEXTAREA') + + await bypassRules.fill('localhost\n127.0.0.1\n*.internal.corp') + await expect(bypassRules).toHaveValue('localhost\n127.0.0.1\n*.internal.corp') + await orcaPage.locator('#settings-http-proxy-url').focus() + + await expect + .poll( + async () => + (await orcaPage.evaluate(() => window.api.settings.get())).httpProxyBypassRules + ) + .toBe('localhost;127.0.0.1;*.internal.corp') + await expect(bypassRules).toHaveValue('localhost;127.0.0.1;*.internal.corp') + } finally { + await orcaPage.evaluate( + (settings) => + window.api.settings.set({ httpProxyBypassRules: settings.httpProxyBypassRules ?? '' }), + original + ) + } + }) +})
diff --git a/tests/e2e/network-proxy-bypass-rules.spec.ts b/tests/e2e/network-proxy-bypass-rules.spec.ts new file mode 100644 index 00000000000..9db71f2fe60 --- /dev/null +++ b/tests/e2e/network-proxy-bypass-rules.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from './helpers/orca-app' +import { waitForSessionReady } from './helpers/store' + +test.describe('network proxy bypass rules', () => { + test('preserves newline-separated hosts and canonicalizes them on blur', async ({ orcaPage }) => { + await waitForSessionReady(orcaPage) + + const original = await orcaPage.evaluate(() => window.api.settings.get()) + try { + await orcaPage.evaluate(() => { + const state = window.__store?.getState() + state?.openSettingsTarget({ pane: 'advanced', repoId: null }) + state?.openSettingsPage() + }) + + await expect(orcaPage.getByRole('heading', { name: 'Advanced', exact: true })).toBeVisible() + await orcaPage.getByRole('button', { name: 'Configure proxy' }).click() + const bypassRules = orcaPage.locator('#settings-http-proxy-bypass-rules') + await expect(bypassRules).toBeVisible() + await expect(bypassRules).toHaveJSProperty('tagName', 'TEXTAREA') + + await bypassRules.fill('localhost\n127.0.0.1\n*.internal.corp') + await expect(bypassRules).toHaveValue('localhost\n127.0.0.1\n*.internal.corp') + await orcaPage.locator('#settings-http-proxy-url').focus() + + await expect + .poll( + async () => + (await orcaPage.evaluate(() => window.api.settings.get())).httpProxyBypassRules + ) + .toBe('localhost;127.0.0.1;*.internal.corp') + await expect(bypassRules).toHaveValue('localhost;127.0.0.1;*.internal.corp') + } finally { + await orcaPage.evaluate( + (settings) => + window.api.settings.set({ httpProxyBypassRules: settings.httpProxyBypassRules ?? '' }), + original + ) + } + }) +})