1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 00:09:40 +02:00

chore(data-cy): require data-cy attributes [EE-6880] (#11453)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run

This commit is contained in:
Ali 2024-04-11 12:11:38 +12:00 committed by GitHub
parent 3cad13388c
commit d38085a560
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
538 changed files with 2571 additions and 595 deletions

View file

@ -11,6 +11,7 @@ import {
import clsx from 'clsx';
import { useDebounce } from '@/react/hooks/useDebounce';
import { AutomationTestingProps } from '@/types';
import { Option } from '@@/form-components/PortainerSelect';
@ -23,6 +24,7 @@ export function AutocompleteSelect({
searchResults,
readOnly,
inputId,
'data-cy': dataCy,
}: {
value: string;
/**
@ -35,7 +37,7 @@ export function AutocompleteSelect({
searchResults?: Option<string>[];
readOnly?: boolean;
inputId: string;
}) {
} & AutomationTestingProps) {
const [searchTerm, setSearchTerm] = useDebounce(value, onChange);
const [selected, setSelected] = useState(false);
@ -54,6 +56,7 @@ export function AutocompleteSelect({
readOnly={readOnly}
id={inputId}
autoComplete="off"
data-cy={dataCy}
/>
{!selected && searchResults && searchResults.length > 0 && (
<ComboboxPopover>

View file

@ -1,6 +1,8 @@
import clsx from 'clsx';
import { PropsWithChildren, ReactNode } from 'react';
import { AutomationTestingProps } from '@/types';
import { ButtonGroup, Size } from '@@/buttons/ButtonGroup';
import { Button } from '@@/buttons';
@ -42,6 +44,7 @@ export function ButtonSelector<T extends string | number | boolean>({
{options.map((option) => (
<OptionItem
key={option.value.toString()}
data-cy={`button-selector-option-${option.value}`}
selected={value === option.value}
onChange={() => onChange(option.value)}
disabled={disabled || option.disabled}
@ -67,7 +70,8 @@ function OptionItem({
onChange,
disabled,
readOnly,
}: PropsWithChildren<OptionItemProps>) {
'data-cy': dataCy,
}: PropsWithChildren<OptionItemProps> & AutomationTestingProps) {
return (
<Button
color="light"
@ -79,10 +83,12 @@ function OptionItem({
},
'!static !z-auto'
)}
data-cy={dataCy}
>
{children}
<input
type="radio"
data-cy={`${dataCy}-radio-input`}
checked={selected}
onChange={onChange}
disabled={disabled}

View file

@ -18,6 +18,7 @@ interface Props extends HTMLProps<HTMLInputElement> {
role?: string;
onChange?: ChangeEventHandler<HTMLInputElement>;
bold?: boolean;
'data-cy': string;
}
export const Checkbox = forwardRef<HTMLInputElement, Props>(
@ -30,6 +31,7 @@ export const Checkbox = forwardRef<HTMLInputElement, Props>(
checked,
onChange,
bold = true,
'data-cy': dataCy,
...props
}: Props,
ref
@ -58,6 +60,7 @@ export const Checkbox = forwardRef<HTMLInputElement, Props>(
ref={resolvedRef}
onChange={onChange}
checked={checked}
data-cy={dataCy}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>

View file

@ -1,5 +1,7 @@
import { List } from 'lucide-react';
import { AutomationTestingProps } from '@/types';
import { CodeEditor } from '@@/CodeEditor';
import { TextTip } from '@@/Tip/TextTip';
import { Button } from '@@/buttons';
@ -11,11 +13,12 @@ export function AdvancedMode({
value,
onChange,
onSimpleModeClick,
'data-cy': dataCy,
}: {
value: Values;
onChange: (value: Values) => void;
onSimpleModeClick: () => void;
}) {
} & AutomationTestingProps) {
const editorValue = convertToArrayOfStrings(value).join('\n');
return (
@ -26,6 +29,7 @@ export function AdvancedMode({
icon={List}
className="!ml-0 p-0 hover:no-underline"
onClick={onSimpleModeClick}
data-cy="env-simple-mode-button"
>
Simple mode
</Button>
@ -40,6 +44,7 @@ export function AdvancedMode({
value={editorValue}
onChange={handleEditorChange}
placeholder="e.g. key=value"
data-cy={dataCy}
/>
</>
);

View file

@ -18,6 +18,7 @@ export function EnvironmentVariableItem({
<div className="w-1/2">
<InputLabeled
className="w-full"
data-cy={`env-name_${index}`}
label="name"
required
value={item.name}
@ -31,7 +32,7 @@ export function EnvironmentVariableItem({
/>
{error && (
<div>
<FormError className="mt-1 !mb-0">
<FormError className="!mb-0 mt-1">
{Object.values(error)[0]}
</FormError>
</div>
@ -39,6 +40,7 @@ export function EnvironmentVariableItem({
</div>
<InputLabeled
className="w-1/2"
data-cy={`env-value_${index}`}
label="value"
value={item.value}
onChange={(e) => handleChange({ value: e.target.value })}

View file

@ -34,6 +34,7 @@ export function EnvironmentVariablesFieldset({
) : (
<AdvancedMode
onSimpleModeClick={() => setSimpleMode(true)}
data-cy="env-var-advanced-mode"
onChange={onChange}
value={values}
/>

View file

@ -34,6 +34,7 @@ export function SimpleMode({
icon={Edit}
className="!ml-0 p-0 hover:no-underline"
onClick={onAdvancedModeClick}
data-cy="environment-variables-advanced-mode-button"
>
Advanced mode
</Button>
@ -50,6 +51,7 @@ export function SimpleMode({
item={EnvironmentVariableItem}
errors={errors}
canUndoDelete={canUndoDelete}
data-cy="simple-environment-variable-fieldset"
/>
<div className="flex gap-2">
@ -60,6 +62,7 @@ export function SimpleMode({
className="!ml-0"
color="default"
icon={Plus}
data-cy="add-environment-variable-button"
>
Add an environment variable
</Button>
@ -84,6 +87,7 @@ function FileEnv({ onChooseFile }: { onChooseFile: (file: Values) => void }) {
accept=".env"
value={file}
color="default"
data-cy="load-environment-variables-from-file-button"
/>
{fileTooBig && (

View file

@ -28,6 +28,7 @@ function Example({ title }: Args) {
value={value}
title={title}
inputId="file-field"
data-cy="file-upload-field"
/>
);
}

View file

@ -7,6 +7,7 @@ test('render should make the file button clickable and fire onChange event after
const { findByText, findByLabelText } = render(
<FileUploadField
title="test button"
data-cy="file-input"
onChange={onClick}
inputId="file-field"
/>

View file

@ -1,12 +1,14 @@
import { ChangeEvent, ComponentProps, createRef } from 'react';
import { Upload, XCircle } from 'lucide-react';
import { AutomationTestingProps } from '@/types';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
import styles from './FileUploadField.module.css';
export interface Props {
export interface Props extends AutomationTestingProps {
onChange(value: File): void;
value?: File | null;
accept?: string;
@ -26,6 +28,7 @@ export function FileUploadField({
inputId,
color = 'primary',
name,
'data-cy': dataCy,
}: Props) {
const fileRef = createRef<HTMLInputElement>();
@ -47,6 +50,7 @@ export function FileUploadField({
color={color}
onClick={handleButtonClick}
className={styles.fileButton}
data-cy={dataCy}
icon={Upload}
>
{title}

View file

@ -31,6 +31,7 @@ function Example({ title }: Args) {
description={
<span>You can upload a Compose file from your computer.</span>
}
data-cy="file-upload-form"
/>
</div>
);

View file

@ -9,6 +9,7 @@ test('render should include description', async () => {
title="test button"
onChange={onClick}
description={<span>test description</span>}
data-cy="test"
/>
);

View file

@ -1,5 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react';
import { AutomationTestingProps } from '@/types';
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { FileUploadField } from '@@/form-components/FileUpload/FileUploadField';
@ -17,7 +19,8 @@ export function FileUploadForm({
title = 'Select a file',
required = false,
description,
}: PropsWithChildren<Props>) {
'data-cy': dataCy,
}: PropsWithChildren<Props> & AutomationTestingProps) {
return (
<div className="file-upload-form">
<FormSectionTitle>Upload</FormSectionTitle>
@ -28,6 +31,7 @@ export function FileUploadForm({
<div className="col-sm-12">
<FileUploadField
inputId="file-upload-field"
data-cy={dataCy}
onChange={onChange}
value={value}
title={title}

View file

@ -42,6 +42,7 @@ function TextField({
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
data-cy="input"
/>
</FormControl>
);
@ -79,6 +80,7 @@ function SelectField({
>
<Select
className="form-control"
data-cy="select"
value={value}
onChange={(e) => setValue(parseInt(e.target.value, 10))}
options={options}

View file

@ -22,6 +22,7 @@ export function TextField({ disabled }: Args) {
value={value}
onChange={(e) => setValue(e.target.value)}
disabled={disabled}
data-cy="docker-logging-options-input"
/>
);
}

View file

@ -1,9 +1,11 @@
import clsx from 'clsx';
import { forwardRef, InputHTMLAttributes, Ref } from 'react';
import { AutomationTestingProps } from '@/types';
export const InputWithRef = forwardRef<
HTMLInputElement,
InputHTMLAttributes<HTMLInputElement>
InputHTMLAttributes<HTMLInputElement> & AutomationTestingProps
>(
// eslint-disable-next-line react/jsx-props-no-spreading
(props, ref) => <Input {...props} mRef={ref} />
@ -14,10 +16,11 @@ export function Input({
mRef: ref,
value,
type,
'data-cy': dataCy,
...props
}: InputHTMLAttributes<HTMLInputElement> & {
mRef?: Ref<HTMLInputElement>;
}) {
} & AutomationTestingProps) {
return (
<input
// eslint-disable-next-line react/jsx-props-no-spreading
@ -26,6 +29,7 @@ export function Input({
value={type === 'number' && Number.isNaN(value) ? '' : value} // avoid the `"NaN" cannot be parsed, or is out of range.` error for an empty number input
ref={ref}
className={clsx('form-control', className)}
data-cy={dataCy}
/>
);
}

View file

@ -16,6 +16,7 @@ function TextInput() {
return (
<InputLabeled
label="label"
data-cy="input"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
@ -28,6 +29,7 @@ function NumberInput() {
return (
<InputLabeled
label="label"
data-cy="input"
type="number"
value={value}
onChange={(e) => setValue(e.target.valueAsNumber)}

View file

@ -1,6 +1,8 @@
import { ComponentProps, InputHTMLAttributes } from 'react';
import clsx from 'clsx';
import { AutomationTestingProps } from '@/types';
import { InputGroup } from '../InputGroup';
export function InputLabeled({
@ -17,7 +19,8 @@ export function InputLabeled({
className?: string;
size?: ComponentProps<typeof InputGroup>['size'];
needsDeletion?: boolean;
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'children'>) {
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'children'> &
AutomationTestingProps) {
return (
<InputGroup
className={clsx(className, needsDeletion && 'striked')}

View file

@ -23,6 +23,7 @@ export function Example({ disabled }: Args) {
return (
<Select
value={value}
data-cy="select"
onChange={(e) => setValue(parseInt(e.target.value, 10))}
disabled={disabled}
options={options}

View file

@ -1,18 +1,23 @@
import clsx from 'clsx';
import { SelectHTMLAttributes } from 'react';
export interface Option<T extends string | number> {
import { AutomationTestingProps } from '@/types';
export interface Option<T extends string | number>
extends Partial<AutomationTestingProps> {
value: T;
label: string;
disabled?: boolean;
}
interface Props<T extends string | number> {
interface Props<T extends string | number> extends AutomationTestingProps {
options: Option<T>[];
}
export function Select<T extends number | string>({
options,
className,
'data-cy': dataCy,
...props
}: Props<T> & SelectHTMLAttributes<HTMLSelectElement>) {
return (
@ -20,9 +25,15 @@ export function Select<T extends number | string>({
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
className={clsx('form-control', className)}
data-cy={dataCy}
>
{options.map((item) => (
<option value={item.value} key={item.value}>
<option
value={item.value}
key={item.value}
disabled={item.disabled}
data-cy={`${dataCy}-${item.value}`}
>
{item.label}
</option>
))}

View file

@ -0,0 +1,10 @@
diff a/app/react/components/form-components/Input/Select.tsx b/app/react/components/form-components/Input/Select.tsx (rejected hunks)
@@ -10,7 +10,7 @@ export interface Option<T extends string | number>
disabled?: boolean;
}
-interface Props<T extends string | number> {
+interface Props<T extends string | number> extends AutomationTestingProps {
options: Option<T>[];
}

View file

@ -1,10 +1,12 @@
import clsx from 'clsx';
import { TextareaHTMLAttributes } from 'react';
import { AutomationTestingProps } from '@/types';
export function TextArea({
className,
...props
}: TextareaHTMLAttributes<HTMLTextAreaElement>) {
}: TextareaHTMLAttributes<HTMLTextAreaElement> & AutomationTestingProps) {
return (
<textarea
// eslint-disable-next-line react/jsx-props-no-spreading

View file

@ -20,6 +20,7 @@ function BasicExample() {
<InputGroup.Addon>@</InputGroup.Addon>
<InputGroup.Input
value={value1}
data-cy="input"
onChange={(e) => setValue1(e.target.value)}
placeholder="Username"
aria-describedby="basic-addon1"
@ -29,6 +30,7 @@ function BasicExample() {
<InputGroup>
<InputGroup.Input
value={value1}
data-cy="input"
onChange={(e) => setValue1(e.target.value)}
placeholder="Recipient's username"
aria-describedby="basic-addon2"
@ -40,6 +42,7 @@ function BasicExample() {
<InputGroup.Addon>$</InputGroup.Addon>
<InputGroup.Input
type="number"
data-cy="input"
value={valueNumber}
onChange={(e) => setValueNumber(parseInt(e.target.value, 10))}
aria-label="Amount (to the nearest dollar)"
@ -52,6 +55,7 @@ function BasicExample() {
<InputGroup.Addon>https://example.com/users/</InputGroup.Addon>
<InputGroup.Input
value={value1}
data-cy="input"
onChange={(e) => setValue1(e.target.value)}
id="basic-url"
aria-describedby="basic-addon3"
@ -75,6 +79,7 @@ function Addons() {
</InputGroup.ButtonWrapper>
<InputGroup.Input
value={value1}
data-cy="input"
onChange={(e) => setValue1(e.target.value)}
/>
</InputGroup>
@ -83,10 +88,11 @@ function Addons() {
<InputGroup>
<InputGroup.Input
value={value2}
data-cy="input"
onChange={(e) => setValue2(e.target.value)}
/>
<InputGroup.Addon>
<input type="checkbox" />
<input type="checkbox" data-cy="checkbox" />
</InputGroup.Addon>
</InputGroup>
</div>
@ -102,6 +108,7 @@ function Sizing() {
<InputGroup.Addon>Small</InputGroup.Addon>
<InputGroup.Input
value={value}
data-cy="input"
onChange={(e) => setValue(e.target.value)}
/>
</InputGroup>
@ -110,6 +117,7 @@ function Sizing() {
<InputGroup.Addon>Default</InputGroup.Addon>
<InputGroup.Input
value={value}
data-cy="input"
onChange={(e) => setValue(e.target.value)}
/>
</InputGroup>
@ -118,6 +126,7 @@ function Sizing() {
<InputGroup.Addon>Large</InputGroup.Addon>
<InputGroup.Input
value={value}
data-cy="input"
onChange={(e) => setValue(e.target.value)}
/>
</InputGroup>

View file

@ -22,6 +22,7 @@ function Defaults() {
label="default example"
value={values}
onChange={(value) => setValues(value)}
data-cy="input-list-default-example"
/>
);
}
@ -37,6 +38,7 @@ function ListWithUndoDeletion() {
value={values}
onChange={(value) => setValues(value)}
canUndoDelete
data-cy="input-list-with-undo-deletion"
/>
);
}
@ -71,6 +73,7 @@ function ListWithInputAndSelect({
movable={movable}
itemBuilder={() => ({ value: 0, select: '', id: values.length })}
tooltip={tooltip}
data-cy="input-list-with-select-and-input"
/>
);
}
@ -96,9 +99,11 @@ function SelectAndInputItem({
onChange={(e) =>
onChange({ ...item, value: parseInt(e.target.value, 10) })
}
data-cy="input"
/>
<Select
onChange={(e) => onChange({ ...item, select: e.target.value })}
data-cy="select"
options={[
{ label: 'option1', value: 'option1' },
{ label: 'option2', value: 'option2' },

View file

@ -3,6 +3,8 @@ import { FormikErrors } from 'formik';
import { ArrowDown, ArrowUp, Plus, RotateCw, Trash2 } from 'lucide-react';
import clsx from 'clsx';
import { AutomationTestingProps } from '@/types';
import { Button } from '@@/buttons';
import { Tooltip } from '@@/Tip/Tooltip';
import { TextTip } from '@@/Tip/TextTip';
@ -53,10 +55,11 @@ type RenderItemFunction<T> = (
item: T,
onChange: (value: T) => void,
index: number,
dataCy: string,
error?: ItemError<T>
) => React.ReactNode;
interface Props<T> {
interface Props<T> extends AutomationTestingProps {
label?: string;
value: T[];
onChange(value: T[], e: OnChangeEvent<T>): void;
@ -71,9 +74,7 @@ interface Props<T> {
errors?: ArrayError<T[]>;
textTip?: string;
isAddButtonHidden?: boolean;
addButtonDataCy?: string;
isDeleteButtonHidden?: boolean;
deleteButtonDataCy?: string;
disabled?: boolean;
addButtonError?: string;
readOnly?: boolean;
@ -95,9 +96,8 @@ export function InputList<T = DefaultType>({
errors,
textTip,
isAddButtonHidden = false,
addButtonDataCy,
isDeleteButtonHidden = false,
deleteButtonDataCy,
'data-cy': dataCy,
disabled,
addButtonError,
readOnly,
@ -161,6 +161,7 @@ export function InputList<T = DefaultType>({
item,
(value: T) => handleChangeItem(key, value),
index,
dataCy,
error
)
)}
@ -173,6 +174,7 @@ export function InputList<T = DefaultType>({
onClick={() => handleMoveUp(index)}
className="vertical-center btn-only-icon"
icon={ArrowUp}
data-cy={`${dataCy}-move-up_${index}`}
/>
<Button
size="medium"
@ -181,6 +183,7 @@ export function InputList<T = DefaultType>({
onClick={() => handleMoveDown(index)}
className="vertical-center btn-only-icon"
icon={ArrowDown}
data-cy={`${dataCy}-move-down_${index}`}
/>
</>
)}
@ -190,7 +193,7 @@ export function InputList<T = DefaultType>({
size="medium"
onClick={() => handleRemoveItem(key, item)}
className="vertical-center btn-only-icon"
data-cy={`${deleteButtonDataCy}_${index}`}
data-cy={`${dataCy}RemoveButton_${index}`}
icon={Trash2}
/>
)}
@ -203,7 +206,7 @@ export function InputList<T = DefaultType>({
initialItemsCount={initialItemsCount.current}
handleRemoveItem={handleRemoveItem}
handleToggleNeedsDeletion={toggleNeedsDeletion}
dataCy={`${deleteButtonDataCy}_${index}`}
dataCy={`${dataCy}RemoveButton_${index}`}
/>
)}
</div>
@ -224,7 +227,7 @@ export function InputList<T = DefaultType>({
className="!ml-0"
size="small"
icon={Plus}
data-cy={addButtonDataCy}
data-cy={`${dataCy}AddButton`}
>
{addLabel}
</Button>
@ -332,7 +335,9 @@ function DefaultItem({
error,
disabled,
readOnly,
}: ItemProps<DefaultType>) {
index,
'data-cy': dataCy,
}: ItemProps<DefaultType> & AutomationTestingProps) {
return (
<>
<Input
@ -341,6 +346,7 @@ function DefaultItem({
className={clsx('!w-full', item.needsDeletion && 'striked')}
disabled={disabled || item.needsDeletion}
readOnly={readOnly}
data-cy={`${dataCy}RemoveButton_${index}`}
/>
{error && <FormError>{error}</FormError>}
</>
@ -351,10 +357,17 @@ function renderDefaultItem(
item: DefaultType,
onChange: (value: DefaultType) => void,
index: number,
dataCy: string,
error?: ItemError<DefaultType>
) {
return (
<DefaultItem item={item} onChange={onChange} error={error} index={index} />
<DefaultItem
item={item}
onChange={onChange}
error={error}
index={index}
data-cy={dataCy}
/>
);
}

View file

@ -14,7 +14,7 @@ export interface Props {
step: number;
value: number;
onChange: (value: number | number[]) => void;
dataCy?: string;
dataCy: string;
// true if you want to always show the tooltip
visibleTooltip?: boolean;
disabled?: boolean;

View file

@ -13,7 +13,15 @@ export function Example() {
setIsChecked(!isChecked);
}
return <Switch name="name" checked={isChecked} onChange={onChange} id="id" />;
return (
<Switch
name="name"
data-cy="switch"
checked={isChecked}
onChange={onChange}
id="id"
/>
);
}
interface Args {
@ -21,7 +29,15 @@ interface Args {
}
function Template({ checked }: Args) {
return <Switch name="name" checked={checked} onChange={() => {}} id="id" />;
return (
<Switch
name="name"
data-cy="switch"
checked={checked}
onChange={() => {}}
id="id"
/>
);
}
export const Checked: Story<Args> = Template.bind({});

View file

@ -8,7 +8,13 @@ function renderDefault({
checked = false,
}: Partial<PropsWithChildren<Props>> = {}) {
return render(
<Switch id="id" name={name} checked={checked} onChange={() => {}} />
<Switch
id="id"
name={name}
checked={checked}
onChange={() => {}}
data-cy="switch"
/>
);
}

View file

@ -16,6 +16,7 @@ export function Example() {
return (
<SwitchField
name="name"
data-cy="switch-field-example"
checked={isChecked}
onChange={onChange}
label="Example"
@ -33,6 +34,7 @@ function Template({ checked, label, labelClass }: Args) {
return (
<SwitchField
name="name"
data-cy="switch-field-example"
checked={checked}
onChange={() => {}}
label={label}

View file

@ -12,6 +12,7 @@ function renderDefault({
return render(
<SwitchField
label={label}
data-cy="switch-field"
name={name}
checked={checked}
onChange={onChange}