fix form validation mode

This commit is contained in:
Ildar Kamalov
2025-01-24 14:49:12 +03:00
parent 254b25a026
commit 681cdb023e
27 changed files with 178 additions and 83 deletions

2
client/package.json vendored
View File

@@ -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",

View File

@@ -29,7 +29,7 @@ const Check = ({ onSubmit }: Props) => {
handleSubmit,
formState: { isDirty, isValid },
} = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
name: '',
},

View File

@@ -26,7 +26,7 @@ const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }:
control,
formState: { isDirty, isSubmitting },
} = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
domain: currentRewrite?.domain || '',
answer: currentRewrite?.answer || '',

View File

@@ -31,7 +31,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
setValue,
formState: { isSubmitting, isDirty },
} = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: initialValues,
});

View File

@@ -36,7 +36,7 @@ export const Form = ({ initialValues, className, setIsLoading }: Props) => {
const history = useHistory();
const { register, watch, setValue } = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
search: initialValues.search || DEFAULT_LOGS_FILTER.search,
response_status: initialValues.response_status || DEFAULT_LOGS_FILTER.response_status,

View File

@@ -63,7 +63,7 @@ export const Form = ({
...defaultFormValues,
...initialValues,
},
mode: 'onChange',
mode: 'onBlur',
});
const {

View File

@@ -23,7 +23,7 @@ type FormDHCPv4Props = {
lease_duration: string;
};
interfaces: any;
onSubmit?: (data: DhcpFormValues) => Promise<void> | 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<DhcpFormValues>();
@@ -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 (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<input
data-testid="v4_gateway_ip"
type="text"
className="form-control"
placeholder={t(ipv4placeholders?.gateway_ip || '')}
@@ -80,6 +75,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<input
data-testid="v4_subnet_mask"
type="text"
className="form-control"
placeholder={t(ipv4placeholders?.subnet_mask || '')}
@@ -106,6 +102,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
<div className="col">
<input
data-testid="v4_range_start"
type="text"
className="form-control"
placeholder={t(ipv4placeholders?.range_start || '')}
@@ -126,6 +123,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
<div className="col">
<input
data-testid="v4_range_end"
type="text"
className="form-control"
placeholder={t(ipv4placeholders?.range_end || '')}
@@ -150,6 +148,7 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<input
data-testid="v4_lease_duration"
type="number"
className="form-control"
placeholder={t(ipv4placeholders?.lease_duration || '')}
@@ -173,7 +172,11 @@ const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={isDisabled}>
<button
data-testid="v4_save"
type="submit"
className="btn btn-success btn-standard"
disabled={isDisabled}>
{t('save_config')}
</button>
</div>

View File

@@ -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 (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
@@ -62,6 +48,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
<div className="col">
<input
data-testid="v6_range_start"
type="text"
className="form-control"
placeholder={t(ipv6placeholders?.range_start || '')}
@@ -70,7 +57,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
validate: {
ipv6: validateIpv6,
required: (value) =>
isEmptyConfig ? undefined : validateRequiredValue(value),
isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value),
},
})}
/>
@@ -83,6 +70,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
<div className="col">
<input
data-testid="v6_range_end"
type="text"
className="form-control"
placeholder={t(ipv6placeholders?.range_end || '')}
@@ -91,7 +79,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
validate: {
ipv6: validateIpv6,
required: (value) =>
isEmptyConfig ? undefined : validateRequiredValue(value),
isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value),
},
})}
/>
@@ -110,6 +98,7 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
<div className="col-lg-6 form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<input
data-testid="v6_lease_duration"
type="number"
className="form-control"
placeholder={t(ipv6placeholders?.lease_duration || '')}
@@ -119,7 +108,8 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
{...register('v6.lease_duration', {
valueAsNumber: true,
validate: {
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
required: (value) =>
isInterfaceIncludesIpv6 ? undefined : validateRequiredValue(value),
},
})}
/>
@@ -130,7 +120,11 @@ const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }
</div>
<div className="btn-list">
<button type="submit" className="btn btn-success btn-standard" disabled={isDisabled}>
<button
data-testid="v6_save"
type="submit"
className="btn btn-success btn-standard"
disabled={isDisabled}>
{t('save_config')}
</button>
</div>

View File

@@ -99,6 +99,7 @@ const Interfaces = () => {
</label>
<select
id="interface_name"
data-testid="interface_name"
className="form-control custom-select pl-4 col-md"
disabled={enabled}
{...register('interface_name', {

View File

@@ -96,7 +96,7 @@ const Dhcp = () => {
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
const methods = useForm<DhcpFormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
v4: v4 || DEFAULT_V4_VALUES,
v6: v6 || DEFAULT_V6_VALUES,
@@ -127,8 +127,14 @@ const Dhcp = () => {
useEffect(() => {
if (v4 || v6 || interfaceName) {
reset({
v4: v4 || DEFAULT_V4_VALUES,
v6: v6 || DEFAULT_V6_VALUES,
v4: {
...DEFAULT_V4_VALUES,
...v4,
},
v6: {
...DEFAULT_V6_VALUES,
...v6,
},
interface_name: interfaceName || '',
});
}

View File

@@ -52,7 +52,7 @@ const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
watch,
formState: { isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
allowed_clients: initialValues?.allowed_clients || '',
disallowed_clients: initialValues?.disallowed_clients || '',

View File

@@ -53,7 +53,7 @@ const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
watch,
formState: { isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
cache_size: initialValues?.cache_size || 0,
cache_ttl_min: initialValues?.cache_ttl_min || 0,

View File

@@ -71,7 +71,7 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
control,
formState: { errors, isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: initialValues,
});

View File

@@ -63,7 +63,7 @@ const Form = ({ initialValues, onSubmit }: FormProps) => {
watch,
formState: { isSubmitting, isDirty },
} = useForm<FormData>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
upstream_dns: initialValues?.upstream_dns || '',
upstream_mode: initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,

View File

@@ -26,6 +26,7 @@ import { Checkbox } from '../../ui/Controls/Checkbox';
import { Radio } from '../../ui/Controls/Radio';
import { Input } from '../../ui/Controls/Input';
import { Textarea } from '../../ui/Controls/Textarea';
import { EncryptionData } from '../../../initialState';
const certificateSourceOptions = [
{
@@ -71,7 +72,7 @@ const validationMessage = (warningValidation: string, isWarning: boolean) => {
);
};
export type FormValues = {
export type EncryptionFormValues = {
enabled: boolean;
serve_plain_dns: boolean;
server_name: string;
@@ -89,7 +90,7 @@ export type FormValues = {
};
type Props = {
initialValues: FormValues;
initialValues: EncryptionFormValues;
processingConfig: boolean;
processingValidate: boolean;
status_key?: string;
@@ -103,10 +104,10 @@ type Props = {
key_type?: string;
issuer?: string;
subject?: string;
onSubmit: (...args: unknown[]) => void;
onChange: (...args: unknown[]) => void;
setTlsConfig: (...args: unknown[]) => void;
validateTlsConfig: (...args: unknown[]) => void;
onSubmit: (values: EncryptionFormValues) => void;
debouncedConfigValidation: (values: EncryptionFormValues) => void;
setTlsConfig: (values: Partial<EncryptionData>) => void;
validateTlsConfig: (values: Partial<EncryptionData>) => void;
};
const defaultValues = {
@@ -141,11 +142,12 @@ export const Form = ({
subject,
warning_validation,
onSubmit,
debouncedConfigValidation,
setTlsConfig,
validateTlsConfig,
}: Props) => {
const { t } = useTranslation();
const previousValuesRef = useRef<FormValues>(initialValues);
const previousValuesRef = useRef<EncryptionFormValues>(initialValues);
const {
control,
@@ -156,31 +158,35 @@ export const Form = ({
setError,
getValues,
formState: { isSubmitting, isValid },
} = useForm<FormValues>({
} = useForm<EncryptionFormValues>({
defaultValues: {
...initialValues,
...defaultValues,
},
mode: 'onChange',
mode: 'onBlur',
});
const watchedValues = watch();
const isEnabled = watch('enabled');
const servePlainDns = watch('serve_plain_dns');
const certificateChain = watch('certificate_chain');
const privateKey = watch('private_key');
const certificatePath = watch('certificate_path');
const privateKeyPath = watch('private_key_path');
const certificateSource = watch('certificate_source');
const privateKeySaved = watch('private_key_saved');
const privateKeySource = watch('key_source');
const {
enabled: isEnabled,
serve_plain_dns: servePlainDns,
certificate_chain: certificateChain,
private_key: privateKey,
certificate_path: certificatePath,
private_key_path: privateKeyPath,
certificate_source: certificateSource,
private_key_saved: privateKeySaved,
key_source: privateKeySource,
} = watchedValues;
useEffect(() => {
const previousValues = previousValuesRef.current;
if (JSON.stringify(previousValues) !== JSON.stringify(watchedValues)) {
// TODO onChange TLS config validation
// TODO(ik) onChange TLS config validation
console.log('debouncedConfigValidation');
debouncedConfigValidation(watchedValues);
previousValuesRef.current = watchedValues;
}
}, [watchedValues]);
@@ -203,7 +209,7 @@ export const Form = ({
}
};
const validatePorts = (values: FormValues) => {
const validatePorts = (values: EncryptionFormValues) => {
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
if (values.port_dns_over_tls && values.port_https) {
@@ -216,12 +222,12 @@ export const Form = ({
return errors;
};
const onFormSubmit = (data: FormValues) => {
const onFormSubmit = (data: EncryptionFormValues) => {
const validationErrors = validatePorts(data);
if (Object.keys(validationErrors).length > 0) {
Object.entries(validationErrors).forEach(([field, message]) => {
setError(field as keyof FormValues, { type: 'manual', message });
setError(field as keyof EncryptionFormValues, { type: 'manual', message });
});
} else {
onSubmit(data);

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { debounce } from 'lodash';
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
import { Form, FormValues } from './Form';
import { EncryptionFormValues, Form } from './Form';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
@@ -11,8 +11,8 @@ import { EncryptionData } from '../../../initialState';
type Props = {
encryption: EncryptionData;
setTlsConfig: (values: EncryptionData) => void;
validateTlsConfig: (values: EncryptionData) => void;
setTlsConfig: (values: Partial<EncryptionData>) => void;
validateTlsConfig: (values: Partial<EncryptionData>) => void;
};
export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {
@@ -24,7 +24,7 @@ export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Prop
}
}, [encryption, validateTlsConfig]);
const getInitialValues = useCallback((data: any): FormValues => {
const getInitialValues = useCallback((data: any): EncryptionFormValues => {
const { certificate_chain, private_key, private_key_saved } = data;
const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
@@ -67,15 +67,16 @@ export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Prop
[getSubmitValues, setTlsConfig],
);
const handleChange = useCallback(
const debouncedConfigValidation = useCallback(
debounce((values) => {
const submitValues = getSubmitValues(values);
if (submitValues.enabled) {
console.log('validateTlsConfig');
validateTlsConfig(submitValues);
}
}, DEBOUNCE_TIMEOUT),
[getSubmitValues, validateTlsConfig],
[],
);
const initialValues = getInitialValues(encryption);
@@ -94,7 +95,7 @@ export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Prop
<Form
initialValues={initialValues}
onSubmit={handleFormSubmit}
onChange={handleChange}
debouncedConfigValidation={debouncedConfigValidation}
setTlsConfig={setTlsConfig}
validateTlsConfig={validateTlsConfig}
{...encryption}

View File

@@ -38,7 +38,7 @@ export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: P
const prevFormValuesRef = useRef<FormValues>(initialValues);
const { register, watch, control } = useForm({
mode: 'onChange',
mode: 'onBlur',
defaultValues: initialValues,
});

View File

@@ -50,7 +50,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
control,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
enabled: initialValues.enabled || false,
anonymize_client_ip: initialValues.anonymize_client_ip || false,

View File

@@ -55,7 +55,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
control,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
...defaultFormValues,
...initialValues,

View File

@@ -67,7 +67,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
control,
formState: { isValid },
} = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
...defaultFormValues,
...initialValues,

View File

@@ -125,7 +125,7 @@ export const validateGatewaySubnetMask = (_: any, allValues: any) => {
* @param allValues
*/
export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
if (!allValues || !allValues.v4 || !value) {
if (!allValues || !allValues.v4 || !value || !allValues.gateway_ip || !allValues.subnet_mask) {
return undefined;
}

View File

@@ -23,7 +23,7 @@ export const Auth = ({ onAuthSubmit }: Props) => {
control,
formState: { isDirty, isValid },
} = useForm<AuthFormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
username: '',
password: '',

View File

@@ -101,7 +101,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
formState: { isValid },
} = useForm({
defaultValues,
mode: 'onChange',
mode: 'onBlur',
});
const watchFields = watch();

View File

@@ -21,7 +21,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
control,
formState: { isValid },
} = useForm<FormValues>({
mode: 'onChange',
mode: 'onBlur',
defaultValues: {
username: '',
password: '',
@@ -39,6 +39,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="username"
type="text"
label={t('username_label')}
placeholder={t('username_placeholder')}
@@ -59,6 +60,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="password"
type="password"
label={t('username_label')}
placeholder={t('password_placeholder')}
@@ -71,7 +73,11 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
</div>
<div className="form-footer">
<button type="submit" className="btn btn-success btn-block" disabled={processing || !isValid}>
<button
data-testid="sign_in"
type="submit"
className="btn btn-success btn-block"
disabled={processing || !isValid}>
{t('sign_in')}
</button>
</div>

View File

@@ -0,0 +1,3 @@
export const ADMIN_USERNAME = 'admin';
export const ADMIN_PASSWORD = 'superpassword';
export const PORT = 3000;

View File

@@ -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);
});
});

View File

@@ -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();
});
});