diff --git a/bamboo-specs/test.yaml b/bamboo-specs/test.yaml index f78992e0a15..f08688041a8 100644 --- a/bamboo-specs/test.yaml +++ b/bamboo-specs/test.yaml @@ -62,7 +62,6 @@ --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ - --output '.' \ --progress 'plain' \ --target 'tester' \ -f ./docker/frontend.Dockerfile \ @@ -222,7 +221,6 @@ --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ - --output '.' \ --progress 'plain' \ --target 'e2etester' \ -f ./docker/frontend.Dockerfile \ diff --git a/client_v2/src/__tests__/clientForm/validators.test.ts b/client_v2/src/__tests__/clientForm/validators.test.ts index 48e08b1f1c0..327989177be 100644 --- a/client_v2/src/__tests__/clientForm/validators.test.ts +++ b/client_v2/src/__tests__/clientForm/validators.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { validateIdentifier, validateUpstreams, + validateBootstrapDns, validateRequiredValue, validateIpv4, validatePort, @@ -503,24 +504,70 @@ describe('validateRewriteNotSame', () => { }); }); -describe('validateUpstreams with bang comments', () => { - it('accepts lines starting with !', () => { - expect(validateUpstreams('! comment line\n8.8.8.8')).toBeUndefined(); +describe('validateUpstreams with bang prefix', () => { + it('rejects lines starting with ! as invalid upstreams', () => { + expect(validateUpstreams('! comment line\n8.8.8.8')).toBeTruthy(); }); - it('accepts only comment lines (!)', () => { - expect(validateUpstreams('! first\n! second')).toBeUndefined(); + it('rejects only ! lines as invalid', () => { + expect(validateUpstreams('! first\n! second')).toBeTruthy(); + }); + + it('rejects !-prefixed URLs that would otherwise pass address check', () => { + expect(validateUpstreams('! https://dns10.quad9.net/dns-query')).toBeTruthy(); + }); + + it('rejects !-prefixed upstream with dot in comment', () => { + expect(validateUpstreams('! dns.example.com:53')).toBeTruthy(); }); it('still accepts # comments', () => { expect(validateUpstreams('# comment\n8.8.8.8')).toBeUndefined(); }); - it('rejects invalid non-comment lines alongside comments', () => { + it('rejects ! and invalid non-comment lines', () => { expect(validateUpstreams('! good\ninvalidline')).toBeTruthy(); }); }); +describe('validateBootstrapDns', () => { + it('returns undefined for empty value', () => { + expect(validateBootstrapDns('')).toBeUndefined(); + }); + + it('returns undefined for valid IP', () => { + expect(validateBootstrapDns('8.8.8.8')).toBeUndefined(); + }); + + it('returns undefined for valid upstream URL', () => { + expect(validateBootstrapDns('https://dns.example.com/dns-query')).toBeUndefined(); + }); + + it('rejects # comment lines (no comment support)', () => { + expect(validateBootstrapDns('# comment\n8.8.8.8')).toBeTruthy(); + }); + + it('rejects #-prefixed URLs that would otherwise pass address check', () => { + expect(validateBootstrapDns('# https://dns.example.com')).toBeTruthy(); + }); + + it('rejects only # lines', () => { + expect(validateBootstrapDns('# first\n# second')).toBeTruthy(); + }); + + it('rejects !-prefixed lines', () => { + expect(validateBootstrapDns('! https://dns.example.com')).toBeTruthy(); + }); + + it('rejects lines without dot or colon', () => { + expect(validateBootstrapDns('not-a-server')).toBe('Invalid format'); + }); + + it('returns line-numbered error for mixed valid and # comment', () => { + expect(validateBootstrapDns('8.8.8.8\n# comment')).toBe('Invalid format on line 2'); + }); +}); + describe('validateLeaseTime', () => { it('accepts 1 (minimum)', () => { expect(validateLeaseTime(1)).toBeUndefined(); diff --git a/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts b/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts index 348e10613e4..e54610866a4 100644 --- a/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts +++ b/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts @@ -44,6 +44,14 @@ describe('validateDomainsPerLine', () => { expect(validateDomainsPerLine('# this is a comment')).toBeUndefined(); }); + it('rejects !-prefixed filter rule that would otherwise pass dot check', () => { + expect(validateDomainsPerLine('! ||example.org^')).toBeTruthy(); + }); + + it('rejects only ! lines as invalid', () => { + expect(validateDomainsPerLine('! first\n! second')).toBeTruthy(); + }); + it('returns undefined for mixed valid lines with comments', () => { expect( validateDomainsPerLine('# comment\nexample.org\n||ads.example.org^'), diff --git a/client_v2/src/common/controls/Textarea/Textarea.tsx b/client_v2/src/common/controls/Textarea/Textarea.tsx index 5fcf1a5b86f..91f3f303aa9 100644 --- a/client_v2/src/common/controls/Textarea/Textarea.tsx +++ b/client_v2/src/common/controls/Textarea/Textarea.tsx @@ -1,23 +1,47 @@ -import { type JSX, Show } from 'solid-js'; +import { type JSX, Show, createSignal, createEffect } from 'solid-js'; import cn from 'clsx'; import s from './styles.module.pcss'; +import { TextareaHighlight } from './TextareaHighlight'; + +import type { CommentLineToken } from 'panel/helpers/constants'; type TextareaChangeEvent = Event & { currentTarget: HTMLTextAreaElement; target: HTMLTextAreaElement; }; -type Props = Omit, 'onChange' | 'onBlur'> & { +type Props = Omit< + JSX.TextareaHTMLAttributes, + 'onChange' | 'onBlur' | 'onInput' | 'onScroll' +> & { label?: JSX.Element; size?: 'small' | 'medium' | 'large'; errorMessage?: string; + highlightComments?: boolean; + commentPrefixes?: readonly CommentLineToken[]; ref?: HTMLTextAreaElement | ((el: HTMLTextAreaElement) => void); onChange?: (event: TextareaChangeEvent) => void; + onInput?: (event: TextareaChangeEvent) => void; onBlur?: (event: FocusEvent) => void; + onScroll?: ( + event: Event & { currentTarget: HTMLTextAreaElement; target: HTMLTextAreaElement }, + ) => void; }; export const Textarea = (props: Props) => { + const [scrollTop, setScrollTop] = createSignal(0); + const [currentValue, setCurrentValue] = createSignal(''); + + // Sync from external prop changes (e.g. dialog open/close resets); + // also handles initial value on mount. Runs before browser paint so + // there is no visible flicker from the empty initial signal. + createEffect(() => { + setCurrentValue(props.value as string); + }); + + const highlightEnabled = () => !!props.highlightComments; + const setRef = (el: HTMLTextAreaElement) => { if (typeof props.ref === 'function') { props.ref(el); @@ -28,10 +52,22 @@ export const Textarea = (props: Props) => { props.onChange?.(e); }; + const handleInput = (e: TextareaChangeEvent) => { + setCurrentValue(e.target.value); + props.onInput?.(e); + }; + const handleBlur = (e: FocusEvent) => { props.onBlur?.(e); }; + const handleScroll = ( + e: Event & { currentTarget: HTMLTextAreaElement; target: HTMLTextAreaElement }, + ) => { + setScrollTop(e.target.scrollTop); + props.onScroll?.(e); + }; + return (
@@ -39,26 +75,38 @@ export const Textarea = (props: Props) => { {props.label} -