From 681cdb023e28efaa03280c356ef67e0a5bbb6f2e Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Fri, 24 Jan 2025 14:49:12 +0300 Subject: [PATCH] fix form validation mode --- client/package.json | 2 +- client/src/components/Filters/Check/index.tsx | 2 +- .../src/components/Filters/Rewrites/Form.tsx | 2 +- .../src/components/Filters/Services/Form.tsx | 2 +- client/src/components/Logs/Filters/Form.tsx | 2 +- .../Settings/Clients/Form/index.tsx | 2 +- .../components/Settings/Dhcp/FormDHCPv4.tsx | 29 +++++---- .../components/Settings/Dhcp/FormDHCPv6.tsx | 32 ++++------ .../components/Settings/Dhcp/Interfaces.tsx | 1 + client/src/components/Settings/Dhcp/index.tsx | 12 +++- .../components/Settings/Dns/Access/Form.tsx | 2 +- .../components/Settings/Dns/Cache/Form.tsx | 2 +- .../components/Settings/Dns/Config/Form.tsx | 2 +- .../components/Settings/Dns/Upstream/Form.tsx | 2 +- .../components/Settings/Encryption/Form.tsx | 50 ++++++++------- .../components/Settings/Encryption/index.tsx | 15 ++--- .../Settings/FiltersConfig/index.tsx | 2 +- .../components/Settings/LogsConfig/Form.tsx | 2 +- .../components/Settings/StatsConfig/Form.tsx | 2 +- .../components/ui/Guide/MobileConfigForm.tsx | 2 +- client/src/helpers/validators.ts | 2 +- client/src/install/Setup/Auth.tsx | 2 +- client/src/install/Setup/Settings.tsx | 2 +- client/src/login/Login/Form.tsx | 10 ++- client/tests/constants.ts | 3 + client/tests/e2e/dhcp.spec.ts | 62 +++++++++++++++++++ client/tests/e2e/login.spec.ts | 13 ++++ 27 files changed, 178 insertions(+), 83 deletions(-) create mode 100644 client/tests/constants.ts create mode 100644 client/tests/e2e/dhcp.spec.ts create mode 100644 client/tests/e2e/login.spec.ts diff --git a/client/package.json b/client/package.json index ba8ae2f3..5dd3365d 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,7 @@ "lint:fix": "eslint './src/**/*.(ts|tsx)' --fix", "test": "jest", "test:watch": "jest --watch", - "test:e2e": "npx playwright test install.spec.ts --headed && npx playwright test test/e2e/dashboard", + "test:e2e": "npx playwright test tests/e2e --headed", "test:e2e:interactive": "npx playwright test --ui", "test:e2e:debug": "npx playwright test --debug", "test:e2e:codegen": "npx playwright codegen", diff --git a/client/src/components/Filters/Check/index.tsx b/client/src/components/Filters/Check/index.tsx index a82c10b6..32024397 100644 --- a/client/src/components/Filters/Check/index.tsx +++ b/client/src/components/Filters/Check/index.tsx @@ -29,7 +29,7 @@ const Check = ({ onSubmit }: Props) => { handleSubmit, formState: { isDirty, isValid }, } = useForm({ - mode: 'onChange', + mode: 'onBlur', defaultValues: { name: '', }, diff --git a/client/src/components/Filters/Rewrites/Form.tsx b/client/src/components/Filters/Rewrites/Form.tsx index 5cbc78dc..b8b30602 100644 --- a/client/src/components/Filters/Rewrites/Form.tsx +++ b/client/src/components/Filters/Rewrites/Form.tsx @@ -26,7 +26,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }: control, formState: { isDirty, isSubmitting }, } = useForm({ - mode: 'onChange', + mode: 'onBlur', defaultValues: { domain: currentRewrite?.domain || '', answer: currentRewrite?.answer || '', diff --git a/client/src/components/Filters/Services/Form.tsx b/client/src/components/Filters/Services/Form.tsx index f478dea4..c8938343 100644 --- a/client/src/components/Filters/Services/Form.tsx +++ b/client/src/components/Filters/Services/Form.tsx @@ -31,7 +31,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet setValue, formState: { isSubmitting, isDirty }, } = useForm({ - mode: 'onChange', + mode: 'onBlur', defaultValues: initialValues, }); diff --git a/client/src/components/Logs/Filters/Form.tsx b/client/src/components/Logs/Filters/Form.tsx index 59dfe4ea..46b92128 100644 --- a/client/src/components/Logs/Filters/Form.tsx +++ b/client/src/components/Logs/Filters/Form.tsx @@ -36,7 +36,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => { const history = useHistory(); const { register, watch, setValue } = useForm({ - mode: 'onChange', + mode: 'onBlur', defaultValues: { search: initialValues.search || DEFAULT_LOGS_FILTER.search, response_status: initialValues.response_status || DEFAULT_LOGS_FILTER.response_status, diff --git a/client/src/components/Settings/Clients/Form/index.tsx b/client/src/components/Settings/Clients/Form/index.tsx index 77c1ff2c..e5195cd2 100644 --- a/client/src/components/Settings/Clients/Form/index.tsx +++ b/client/src/components/Settings/Clients/Form/index.tsx @@ -63,7 +63,7 @@ export const Form = ({ ...defaultFormValues, ...initialValues, }, - mode: 'onChange', + mode: 'onBlur', }); const { diff --git a/client/src/components/Settings/Dhcp/FormDHCPv4.tsx b/client/src/components/Settings/Dhcp/FormDHCPv4.tsx index 13cc648c..729b6a80 100644 --- a/client/src/components/Settings/Dhcp/FormDHCPv4.tsx +++ b/client/src/components/Settings/Dhcp/FormDHCPv4.tsx @@ -23,7 +23,7 @@ type FormDHCPv4Props = { lease_duration: string; }; interfaces: any; - onSubmit?: (data: DhcpFormValues) => Promise | void; + onSubmit?: (data: DhcpFormValues) => void; }; const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }: FormDHCPv4Props) => { @@ -32,7 +32,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit } const { register, handleSubmit, - formState: { errors, isSubmitting, isValid }, + formState: { errors, isSubmitting }, watch, } = useFormContext(); @@ -41,25 +41,20 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit } const formValues = watch('v4'); const isEmptyConfig = !Object.values(formValues || {}).some(Boolean); - - const handleFormSubmit = async (data: DhcpFormValues) => { - // TODO handle submit - if (onSubmit) { - await onSubmit(data); - } - }; + const hasV4Errors = errors.v4 && Object.keys(errors.v4).length > 0; const isDisabled = useMemo(() => { - return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig; - }, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]); + return isSubmitting || hasV4Errors || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig; + }, [isSubmitting, hasV4Errors, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]); return ( -
+
-
diff --git a/client/src/components/Settings/Dhcp/FormDHCPv6.tsx b/client/src/components/Settings/Dhcp/FormDHCPv6.tsx index d4dcd246..16facdb4 100644 --- a/client/src/components/Settings/Dhcp/FormDHCPv6.tsx +++ b/client/src/components/Settings/Dhcp/FormDHCPv6.tsx @@ -6,14 +6,6 @@ import { UINT32_RANGE } from '../../../helpers/constants'; import { validateIpv6, validateRequiredValue } from '../../../helpers/validators'; import { DhcpFormValues } from '.'; -type FormValues = { - v6?: { - range_start?: string; - range_end?: string; - lease_duration?: number; - }; -}; - type FormDHCPv6Props = { processingConfig?: boolean; ipv6placeholders?: { @@ -40,18 +32,12 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit } const formValues = watch('v6'); const isEmptyConfig = !Object.values(formValues || {}).some(Boolean); - const handleFormSubmit = async (data: FormValues) => { - if (onSubmit) { - await onSubmit(data); - } - }; - const isDisabled = useMemo(() => { return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig; }, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv6, isEmptyConfig]); return ( - +
@@ -62,6 +48,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
- isEmptyConfig ? undefined : validateRequiredValue(value), + isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value), }, })} /> @@ -83,6 +70,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
- isEmptyConfig ? undefined : validateRequiredValue(value), + isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value), }, })} /> @@ -110,6 +98,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
(isEmptyConfig ? undefined : validateRequiredValue(value)), + required: (value) => + isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value), }, })} /> @@ -130,7 +120,11 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
-
diff --git a/client/src/components/Settings/Dhcp/Interfaces.tsx b/client/src/components/Settings/Dhcp/Interfaces.tsx index ac2d6b02..2ab32de7 100644 --- a/client/src/components/Settings/Dhcp/Interfaces.tsx +++ b/client/src/components/Settings/Dhcp/Interfaces.tsx @@ -99,6 +99,7 @@ const Interfaces = () => { { render={({ field, fieldState }) => ( {
-
diff --git a/client/tests/constants.ts b/client/tests/constants.ts new file mode 100644 index 00000000..e3634d6e --- /dev/null +++ b/client/tests/constants.ts @@ -0,0 +1,3 @@ +export const ADMIN_USERNAME = 'admin'; +export const ADMIN_PASSWORD = 'superpassword'; +export const PORT = 3000; diff --git a/client/tests/e2e/dhcp.spec.ts b/client/tests/e2e/dhcp.spec.ts new file mode 100644 index 00000000..6f44dd3c --- /dev/null +++ b/client/tests/e2e/dhcp.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants'; + +const INTERFACE_NAME = 'en0'; +const RANGE_START = '192.168.1.100'; +const RANGE_END = '192.168.1.200'; +const SUBNET_MASK = '255.255.255.0'; +const LEASE_TIME = '86400'; + +test.describe('DHCP Configuration', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/login.html'); + await page.getByTestId('username').click(); + await page.getByTestId('username').fill(ADMIN_USERNAME); + await page.getByTestId('password').click(); + await page.getByTestId('password').fill(ADMIN_PASSWORD); + await page.getByTestId('sign_in').click(); + await page.getByText('Settings', { exact: true }).click(); + await page.goto(`/#dhcp`); + }); + + test('should select the correct DHCP interface', async ({ page }) => { + await page.getByTestId('interface_name').selectOption(INTERFACE_NAME); + expect(await page.locator('select[name="interface_name"]').inputValue()).toBe(INTERFACE_NAME); + }); + + test('should configure DHCP IPv4 settings correctly', async ({ page }) => { + await page.getByTestId('interface_name').selectOption(INTERFACE_NAME); + await page.getByTestId('v4_gateway_ip').click(); + await page.getByTestId('v4_gateway_ip').fill('192.168.1.99'); + await page.getByTestId('v4_subnet_mask').click(); + await page.getByTestId('v4_subnet_mask').fill(SUBNET_MASK); + await page.getByTestId('v4_range_start').click(); + await page.getByTestId('v4_range_start').fill(RANGE_START); + await page.getByTestId('v4_range_end').click(); + await page.getByTestId('v4_range_end').fill(RANGE_END); + await page.getByTestId('v4_lease_duration').click(); + await page.getByTestId('v4_lease_duration').fill(LEASE_TIME); + await page.getByTestId('v4_save').click(); + }); + + test('should show error for invalid DHCP IPv4 range', async ({ page }) => { + await page.getByTestId('interface_name').selectOption(INTERFACE_NAME); + await page.getByTestId('v4_range_start').click(); + await page.getByTestId('v4_range_start').fill(RANGE_END); + await page.getByTestId('v4_range_end').click(); + await page.getByTestId('v4_range_end').fill(RANGE_START); + await page.locator('.col-12').first().click(); + await page.getByText('Must be greater than range').click(); + + expect(await page.getByText('Must be greater than range').isVisible()).toBe(true); + }); + + test('should show error for invalid DHCP IPv4 address', async ({ page }) => { + await page.getByTestId('interface_name').selectOption(INTERFACE_NAME); + await page.getByTestId('v4_gateway_ip').click(); + await page.getByTestId('v4_gateway_ip').fill('192.168.1.200s'); + await page.getByText('Invalid IPv4 address').click(); + + expect(await page.getByText('Invalid IPv4 address').isVisible()).toBe(true); + }); +}); diff --git a/client/tests/e2e/login.spec.ts b/client/tests/e2e/login.spec.ts new file mode 100644 index 00000000..8c0553b3 --- /dev/null +++ b/client/tests/e2e/login.spec.ts @@ -0,0 +1,13 @@ +import { test } from '@playwright/test'; +import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants'; + +test.describe('Login', () => { + test('should successfully log in with valid credentials', async ({ page }) => { + await page.goto(`/login.html`); + await page.getByTestId('username').click(); + await page.getByTestId('username').fill(ADMIN_USERNAME); + await page.getByTestId('password').click(); + await page.getByTestId('password').fill(ADMIN_PASSWORD); + await page.getByTestId('sign_in').click(); + }); +});