1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +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

@ -1,11 +1,12 @@
import { Briefcase } from 'lucide-react';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { AutomationTestingProps } from '@/types';
import { Button } from '@@/buttons';
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
interface Props {
interface Props extends AutomationTestingProps {
featureId: FeatureId;
heading: string;
message: string;
@ -21,6 +22,7 @@ export function BETeaserButton({
buttonText,
className,
buttonClassName,
'data-cy': dataCy,
}: Props) {
return (
<TooltipWithChildren
@ -38,6 +40,7 @@ export function BETeaserButton({
size="small"
onClick={() => {}}
disabled
data-cy={dataCy}
>
{buttonText}
</Button>

View file

@ -19,7 +19,12 @@ export function Code({ children, showCopyButton }: Props) {
<code className={styles.code}>{children}</code>
{showCopyButton && (
<Button color="link" className={styles.copyButton} onClick={handleCopy}>
<Button
color="link"
className={styles.copyButton}
onClick={handleCopy}
data-cy="code-copy-button"
>
<Icon
icon={copiedSuccessfully ? Check : Copy}
className="!ml-1"

View file

@ -7,12 +7,14 @@ import { useMemo } from 'react';
import { createTheme } from '@uiw/codemirror-themes';
import { tags as highlightTags } from '@lezer/highlight';
import { AutomationTestingProps } from '@/types';
import { CopyButton } from '@@/buttons/CopyButton';
import styles from './CodeEditor.module.css';
import { TextTip } from './Tip/TextTip';
interface Props {
interface Props extends AutomationTestingProps {
id: string;
placeholder?: string;
yaml?: boolean;
@ -67,6 +69,7 @@ export function CodeEditor({
yaml: isYaml,
dockerFile: isDockerFile,
shell: isShell,
'data-cy': dataCy,
}: Props) {
const extensions = useMemo(() => {
const extensions = [];
@ -90,6 +93,7 @@ export function CodeEditor({
</div>
<CopyButton
data-cy={`copy-code-button-${id}`}
fadeDelay={2500}
copyText={value}
color="link"
@ -112,6 +116,7 @@ export function CodeEditor({
highlightSelectionMatches: false,
autocompletion: false,
}}
data-cy={dataCy}
/>
</>
);

View file

@ -19,7 +19,14 @@ interface StoryProps {
}
function Template({ value, icon, type }: StoryProps) {
return <DashboardItem value={value} icon={icon} type={type} />;
return (
<DashboardItem
value={value}
icon={icon}
type={type}
dataCy="data-cy-example"
/>
);
}
export const Primary: Story<StoryProps> = Template.bind({});
@ -31,15 +38,25 @@ Primary.args = {
export function WithLink() {
return (
<Link to="example.page">
<DashboardItem value={1} icon={List} type="Example resource" />
<Link to="example.page" data-cy="data-cy-example">
<DashboardItem
value={1}
icon={List}
type="Example resource"
dataCy="data-cy-example"
/>
</Link>
);
}
export function WithChildren() {
return (
<DashboardItem value={1} icon={List} type="Example resource">
<DashboardItem
value={1}
icon={List}
type="Example resource"
dataCy="data-cy-example"
>
<div>Children</div>
</DashboardItem>
);

View file

@ -26,5 +26,7 @@ test('should have accessibility label created from the provided resource type',
});
function renderComponent(value = 0, icon = User, type = '') {
return render(<DashboardItem value={value} icon={icon} type={type} />);
return render(
<DashboardItem value={value} icon={icon} type={type} dataCy="example" />
);
}

View file

@ -16,7 +16,7 @@ interface Props extends IconProps {
to?: string;
params?: object;
children?: ReactNode;
dataCy?: string;
dataCy: string;
}
export function DashboardItem({
@ -103,7 +103,12 @@ export function DashboardItem({
if (to) {
return (
<Link to={to} className="!no-underline" params={params}>
<Link
to={to}
className="!no-underline"
params={params}
data-cy={`${dataCy}-link`}
>
{Item}
</Link>
);

View file

@ -17,7 +17,7 @@ export default {
function Template({ key1, val1, key2, val2 }: Args) {
return (
<DetailsTable>
<DetailsTable dataCy="details-table">
<DetailsRow label={key1}>{val1}</DetailsRow>
<DetailsRow label={key2}>{val2}</DetailsRow>
</DetailsTable>

View file

@ -10,7 +10,7 @@ test('should display child row elements', () => {
};
const { queryByText } = render(
<DetailsTable>
<DetailsTable dataCy="details-table">
<DetailsTable.Row label="Name">{person.name}</DetailsTable.Row>
<DetailsTable.Row label="Id">{person.id}</DetailsTable.Row>
</DetailsTable>

View file

@ -2,8 +2,8 @@ import clsx from 'clsx';
import { Children, PropsWithChildren } from 'react';
type Props = {
dataCy: string;
headers?: string[];
dataCy?: string;
className?: string;
emptyMessage?: string;
};

View file

@ -34,6 +34,7 @@ export function AdvancedForm({
}}
placeholder="e.g. registry:port/my-image:my-tag"
required
data-cy="image-config-advanced-input"
/>
</FormControl>
</>

View file

@ -46,6 +46,7 @@ export function ImageConfigFieldset({
icon={Globe}
className="!ml-0 p-0 hover:no-underline"
onClick={() => setFieldValue('useRegistry', false)}
data-cy="image-config-advanced-button"
>
Advanced mode
</Button>
@ -56,6 +57,7 @@ export function ImageConfigFieldset({
icon={Database}
className="!ml-0 p-0 hover:no-underline"
onClick={() => setFieldValue('useRegistry', true)}
data-cy="image-config-simple-button"
>
Simple mode
</Button>

View file

@ -11,6 +11,7 @@ export function InputSearch({
options,
placeholder,
inputId,
'data-cy': dataCy,
}: {
value: string;
onChange: (value: string) => void;
@ -34,6 +35,7 @@ export function InputSearch({
onChange={onChange}
placeholder={placeholder}
inputId={inputId}
data-cy={dataCy}
/>
);
}

View file

@ -93,8 +93,13 @@ function RateLimitsInner({
You are currently using an anonymous account to pull images
from DockerHub and will be limited to 100 pulls every 6
hours. You can configure DockerHub authentication in the{' '}
<Link to="portainer.registries">Registries View</Link>.
Remaining pulls:{' '}
<Link
to="portainer.registries"
data-cy="image-registry-rate-limits-registries-view-link"
>
Registries View
</Link>
. Remaining pulls:{' '}
<span className="font-bold">
{pullRateLimits.remaining}/{pullRateLimits.limit}
</span>

View file

@ -91,6 +91,7 @@ export function SimpleForm({
rel: 'noreferrer',
}}
icon={DockerIcon}
data-cy="component-dockerHubSearchButton"
>
Search
</Button>
@ -189,6 +190,7 @@ function ImageField({
value={value}
onChange={(e) => onChange(e.target.value)}
id={inputId}
data-cy="image-field-simple-input"
/>
);
}

View file

@ -29,7 +29,12 @@ export function InformationPanel({
<span>{title}</span>
{!!onDismiss && (
<span className="small" style={{ float: 'right' }}>
<Button color="link" icon={X} onClick={() => onDismiss()}>
<Button
color="link"
icon={X}
onClick={() => onDismiss()}
data-cy="dismiss-information-panel-button"
>
dismiss
</Button>
</span>

View file

@ -63,6 +63,7 @@ export function InsightsBox({
{insightCloseId && (
<Button
icon={X}
data-cy={`insight-close-${insightCloseId}`}
className={clsx(
'absolute right-2 top-3 flex !text-gray-7 hover:!text-gray-8 th-highcontrast:!text-gray-6 th-highcontrast:hover:!text-gray-5 th-dark:!text-gray-6 th-dark:hover:!text-gray-5',
type === 'slim' && insightCloseId && 'top-1'

View file

@ -5,10 +5,12 @@ interface Props {
title?: string;
target?: AnchorHTMLAttributes<HTMLAnchorElement>['target'];
rel?: AnchorHTMLAttributes<HTMLAnchorElement>['rel'];
'data-cy': AnchorHTMLAttributes<HTMLAnchorElement>['data-cy'];
}
export function Link({
children,
'data-cy': dataCy,
to,
params,
options,
@ -18,7 +20,7 @@ export function Link({
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<a onClick={onClick} href={href} {...props}>
<a onClick={onClick} href={href} data-cy={dataCy} {...props}>
{children}
</a>
);

View file

@ -11,6 +11,7 @@ export function LinkButton({
className,
children,
title = '',
'data-cy': dataCy,
...props
}: ComponentProps<typeof Button> & ComponentProps<typeof Link>) {
return (
@ -24,7 +25,9 @@ export function LinkButton({
props={{
to,
params,
'data-cy': `${dataCy}-link`,
}}
data-cy={`${dataCy}-button`}
>
{children}
</Button>

View file

@ -43,6 +43,7 @@ function renderCrumb(crumb: Crumb | string) {
to={crumb.link}
params={crumb.linkParams}
className="text-blue-9 hover:text-blue-11 hover:underline th-highcontrast:text-blue-5 th-dark:text-blue-7 th-dark:hover:text-blue-9"
data-cy={`breadcrumb-${crumb.label}`}
>
{crumb.label}
</Link>

View file

@ -25,6 +25,7 @@ export function ContextHelp() {
)}
title="Help"
rel="noreferrer"
data-cy="context-help-button"
>
<HelpCircle className="lucide" />
</a>

View file

@ -16,7 +16,10 @@ export function HeaderTitle({ title, children }: PropsWithChildren<Props>) {
return (
<div className="flex justify-between whitespace-normal pt-3">
<div className="flex items-center gap-2">
<h1 className="m-0 text-2xl font-medium text-gray-11 th-highcontrast:text-white th-dark:text-white">
<h1
className="m-0 text-2xl font-medium text-gray-11 th-highcontrast:text-white th-dark:text-white"
data-cy="page-title"
>
{title}
</h1>
{children && <>{children}</>}

View file

@ -97,19 +97,25 @@ export function NotificationsMenu() {
{reducedNotifications?.length > 0 ? (
<>
<div className={notificationStyles.notifications}>
{reducedNotifications.map((notification) => (
{reducedNotifications.map((notification, index) => (
<MenuLink
to="portainer.notifications"
params={{ id: notification.id }}
notification={notification}
key={notification.id}
onDelete={() => onDelete(notification.id)}
data-cy={`notification-delete-button_${index}`}
/>
))}
</div>
<div className={notificationStyles.notificationLink}>
<Link to="portainer.notifications">View all notifications</Link>
<Link
to="portainer.notifications"
data-cy="notifications-see-all-link"
>
View all notifications
</Link>
</div>
</>
) : (

View file

@ -44,6 +44,7 @@ export function PageHeader({
className="m-0 p-0 focus:text-inherit"
disabled={loading}
title="Refresh page"
data-cy="refresh-page-button"
>
<RefreshCw className="icon" />
</Button>

View file

@ -38,8 +38,13 @@ export function PageInput({ onChange, totalPages }: Props) {
step={1}
onChange={handleChange}
onKeyPress={preventNotNumber}
data-cy="pagination-go-to-page-input"
/>
<Button type="submit" disabled={!isValid}>
<Button
type="submit"
disabled={!isValid}
data-cy="pagination-go-to-page-button"
>
Go
</Button>
</form>

View file

@ -27,6 +27,7 @@ export function RadioGroup<T extends string | number = string>({
checked={selectedOption === option.value}
onChange={() => onOptionChange(option.value)}
style={{ margin: '0 4px 0 0' }}
data-cy={`radio-${option.value}`}
/>
{option.label}
</label>

View file

@ -26,12 +26,14 @@ function Template({ totalSteps = 5 }: Args) {
<Stepper currentStep={currentStep} steps={steps} />
<Button
onClick={() => setCurrentStep(currentStep - 1)}
data-cy="previous-button"
disabled={currentStep <= 1}
>
Previous
</Button>
<Button
onClick={() => setCurrentStep(currentStep + 1)}
data-cy="next-button"
disabled={currentStep >= steps.length}
>
Next

View file

@ -21,9 +21,10 @@ export function TLSFieldset({ values, onChange, errors }: Props) {
<div className="col-sm-12">
<SwitchField
label="TLS"
labelClass="col-sm-3 col-lg-2"
data-cy="enable-tls-switch"
checked={values.tls}
onChange={(checked) => handleChange({ tls: checked })}
labelClass="col-sm-3 col-lg-2"
/>
</div>
</div>
@ -34,6 +35,7 @@ export function TLSFieldset({ values, onChange, errors }: Props) {
<div className="col-sm-12">
<SwitchField
label="Skip Certification Verification"
data-cy="skip-verify-switch"
checked={!!values.skipVerify}
onChange={(checked) => handleChange({ skipVerify: checked })}
labelClass="col-sm-3 col-lg-2"
@ -50,6 +52,7 @@ export function TLSFieldset({ values, onChange, errors }: Props) {
>
<FileUploadField
inputId="ca-cert-field"
data-cy="ca-cert-file-upload"
onChange={(file) => handleChange({ caCertFile: file })}
value={values.caCertFile}
/>
@ -61,6 +64,7 @@ export function TLSFieldset({ values, onChange, errors }: Props) {
>
<FileUploadField
inputId="cert-field"
data-cy="cert-file-upload"
onChange={(file) => handleChange({ certFile: file })}
value={values.certFile}
/>
@ -72,6 +76,7 @@ export function TLSFieldset({ values, onChange, errors }: Props) {
>
<FileUploadField
inputId="tls-key-field"
data-cy="tls-key-file-upload"
onChange={(file) => handleChange({ keyFile: file })}
value={values.keyFile}
/>

View file

@ -45,7 +45,11 @@ export function TagSelector({ value, allowCreate = false, onChange }: Props) {
<div className="form-group">
<div className="col-sm-12 small text-muted">
No tags available. Head over to the
<Link to="portainer.tags" className="space-right space-left">
<Link
to="portainer.tags"
className="space-right space-left"
data-cy="environment-tags-view-link"
>
Tags view
</Link>
to add tags
@ -82,6 +86,7 @@ export function TagSelector({ value, allowCreate = false, onChange }: Props) {
formatCreateLabel={(inputValue) => `Create "${inputValue}"`}
onCreateOption={handleCreateOption}
aria-label="Tags"
data-cy="environment-tags-selector"
/>
</FormControl>
</>

View file

@ -23,6 +23,7 @@ function Example() {
onChange={(value) => setSelectedTeams(value)}
teams={teams}
placeholder="Select one or more teams"
dataCy="teams-selector"
/>
);
}

View file

@ -7,7 +7,7 @@ interface Props {
value: TeamId[] | readonly TeamId[];
onChange(value: readonly TeamId[]): void;
teams: Team[];
dataCy?: string;
dataCy: string;
inputId?: string;
placeholder?: string;
disabled?: boolean;

View file

@ -24,6 +24,7 @@ function Example() {
onChange={setSelectedUsers}
users={users}
placeholder="Select one or more users"
dataCy="users-selector"
/>
);
}

View file

@ -7,7 +7,7 @@ interface Props {
value: UserId[];
onChange(value: UserId[]): void;
users: User[];
dataCy?: string;
dataCy: string;
inputId?: string;
placeholder?: string;
disabled?: boolean;

View file

@ -2,6 +2,7 @@ import { PropsWithChildren, useEffect, useMemo } from 'react';
import { useTransitionHook } from '@uirouter/react';
import { BROWSER_OS_PLATFORM } from '@/react/constants';
import { AutomationTestingProps } from '@/types';
import { CodeEditor } from '@@/CodeEditor';
import { Tooltip } from '@@/Tip/Tooltip';
@ -51,7 +52,7 @@ export const editorConfig = {
win: otherEditorConfig,
} as const;
interface Props {
interface Props extends AutomationTestingProps {
value: string;
onChange: (value: string) => void;
@ -77,6 +78,7 @@ export function WebEditorForm({
children,
error,
height,
'data-cy': dataCy,
}: PropsWithChildren<Props>) {
return (
<div>
@ -105,6 +107,7 @@ export function WebEditorForm({
value={value}
onChange={onChange}
height={height}
data-cy={dataCy}
/>
</div>
</div>

View file

@ -46,6 +46,7 @@ export function WidgetTabs({ currentTabIndex, tabs }: Props) {
currentTabIndex !== index,
}
)}
data-cy={`tab-${index}`}
>
<Icon icon={icon} />
{name}

View file

@ -12,7 +12,7 @@ type Args = {
};
function Template({ label }: Args) {
return <AddButton>{label}</AddButton>;
return <AddButton data-cy="add-">{label}</AddButton>;
}
export const Primary: Story<Args> = Template.bind({});

View file

@ -28,7 +28,11 @@ function renderDefault({
],
route: 'root',
});
return render(<Wrapped to="">{label}</Wrapped>);
return render(
<Wrapped to="" data-cy="wrapped">
{label}
</Wrapped>
);
}
test('should display a AddButton component', async () => {

View file

@ -25,10 +25,10 @@ export function AddButton({
return (
<Button
as={Link}
props={{ to, params }}
props={{ to, params, 'data-cy': `${dataCy}-link` }}
icon={Plus}
className="!m-0"
data-cy={dataCy}
data-cy={`${dataCy}-button`}
color={color}
disabled={disabled}
>

View file

@ -50,6 +50,7 @@ export function DifferentTheme() {
{states.map((state) => (
<Button
color={color}
data-cy="button"
key={state}
disabled={state === 'disabled'}
>
@ -72,7 +73,13 @@ function Template({
disabled,
}: JSX.IntrinsicAttributes & PropsWithChildren<Props>) {
return (
<Button onClick={onClick} color={color} size={size} disabled={disabled}>
<Button
onClick={onClick}
color={color}
size={size}
disabled={disabled}
data-cy="button"
>
Primary Button
</Button>
);
@ -90,7 +97,7 @@ Primary.args = {
export function Disabled() {
return (
<Button color="primary" onClick={() => {}} disabled>
<Button color="primary" onClick={() => {}} disabled data-cy="button">
Disabled Button
</Button>
);
@ -98,7 +105,7 @@ export function Disabled() {
export function Danger() {
return (
<Button color="danger" onClick={() => {}}>
<Button color="danger" onClick={() => {}} data-cy="button">
Danger Button
</Button>
);
@ -106,7 +113,7 @@ export function Danger() {
export function ButtonIcon() {
return (
<Button color="primary" onClick={() => {}} icon={Download}>
<Button color="primary" onClick={() => {}} icon={Download} data-cy="button">
Button with an icon
</Button>
);
@ -114,7 +121,13 @@ export function ButtonIcon() {
export function ButtonIconLarge() {
return (
<Button color="primary" onClick={() => {}} icon={Download} size="large">
<Button
color="primary"
onClick={() => {}}
icon={Download}
size="large"
data-cy="button"
>
Button with an icon
</Button>
);
@ -122,7 +135,13 @@ export function ButtonIconLarge() {
export function ButtonIconMedium() {
return (
<Button color="primary" onClick={() => {}} icon={Download} size="medium">
<Button
color="primary"
onClick={() => {}}
icon={Download}
size="medium"
data-cy="button"
>
Button with an icon
</Button>
);
@ -130,7 +149,13 @@ export function ButtonIconMedium() {
export function ButtonIconXSmall() {
return (
<Button color="primary" onClick={() => {}} icon={Download} size="xsmall">
<Button
color="primary"
onClick={() => {}}
icon={Download}
size="xsmall"
data-cy="button"
>
Button with an icon
</Button>
);
@ -138,7 +163,7 @@ export function ButtonIconXSmall() {
export function Default() {
return (
<Button color="default" onClick={() => {}}>
<Button color="default" onClick={() => {}} data-cy="button">
Default
</Button>
);
@ -146,7 +171,7 @@ export function Default() {
export function Link() {
return (
<Button color="link" onClick={() => {}}>
<Button color="link" onClick={() => {}} data-cy="button">
Link Button
</Button>
);
@ -154,7 +179,7 @@ export function Link() {
export function XSmall() {
return (
<Button color="primary" onClick={() => {}} size="xsmall">
<Button color="primary" onClick={() => {}} size="xsmall" data-cy="button">
XSmall Button
</Button>
);
@ -162,7 +187,7 @@ export function XSmall() {
export function Small() {
return (
<Button color="primary" onClick={() => {}} size="small">
<Button color="primary" onClick={() => {}} size="small" data-cy="button">
Small Button
</Button>
);
@ -170,7 +195,7 @@ export function Small() {
export function Large() {
return (
<Button color="primary" onClick={() => {}} size="large">
<Button color="primary" onClick={() => {}} size="large" data-cy="button">
Large Button
</Button>
);

View file

@ -14,6 +14,7 @@ function renderDefault({
return render(
<Button
type={type}
data-cy="button"
color={color}
size={size}
disabled={disabled}

View file

@ -15,19 +15,30 @@ function Template({
}: JSX.IntrinsicAttributes & PropsWithChildren<Props>) {
return (
<ButtonGroup size={size}>
<Button icon={Play} color="primary" onClick={() => {}}>
<Button icon={Play} color="primary" onClick={() => {}} data-cy="button">
Start
</Button>
<Button icon={Square} color="danger" onClick={() => {}}>
<Button icon={Square} color="danger" onClick={() => {}} data-cy="button">
Stop
</Button>
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
<Button
icon={RefreshCw}
color="primary"
onClick={() => {}}
data-cy="button"
>
Restart
</Button>
<Button icon={Play} color="primary" disabled onClick={() => {}}>
<Button
icon={Play}
color="primary"
disabled
onClick={() => {}}
data-cy="button"
>
Resume
</Button>
<Button icon={Trash2} color="danger" onClick={() => {}}>
<Button icon={Trash2} color="danger" onClick={() => {}} data-cy="button">
Remove
</Button>
</ButtonGroup>
@ -42,16 +53,21 @@ Primary.args = {
export function Xsmall() {
return (
<ButtonGroup size="xsmall">
<Button icon={Play} color="primary" onClick={() => {}}>
<Button icon={Play} color="primary" onClick={() => {}} data-cy="button">
Start
</Button>
<Button icon={Square} color="danger" onClick={() => {}}>
<Button icon={Square} color="danger" onClick={() => {}} data-cy="button">
Stop
</Button>
<Button icon={Play} color="primary" onClick={() => {}}>
<Button icon={Play} color="primary" onClick={() => {}} data-cy="button">
Start
</Button>
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
<Button
icon={RefreshCw}
color="primary"
onClick={() => {}}
data-cy="button"
>
Restart
</Button>
</ButtonGroup>
@ -61,16 +77,21 @@ export function Xsmall() {
export function Small() {
return (
<ButtonGroup size="small">
<Button icon={Play} color="primary" onClick={() => {}}>
<Button icon={Play} color="primary" onClick={() => {}} data-cy="button">
Start
</Button>
<Button icon={Square} color="danger" onClick={() => {}}>
<Button icon={Square} color="danger" onClick={() => {}} data-cy="button">
Stop
</Button>
<Button icon={Play} color="primary" onClick={() => {}}>
<Button icon={Play} color="primary" onClick={() => {}} data-cy="button">
Start
</Button>
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
<Button
icon={RefreshCw}
color="primary"
onClick={() => {}}
data-cy="button"
>
Restart
</Button>
</ButtonGroup>
@ -80,16 +101,21 @@ export function Small() {
export function Large() {
return (
<ButtonGroup size="large">
<Button icon={Play} color="primary" onClick={() => {}}>
<Button icon={Play} color="primary" onClick={() => {}} data-cy="button">
Start
</Button>
<Button icon={Square} color="danger" onClick={() => {}}>
<Button icon={Square} color="danger" onClick={() => {}} data-cy="button">
Stop
</Button>
<Button icon={Play} color="light" onClick={() => {}}>
<Button icon={Play} color="light" onClick={() => {}} data-cy="button">
Start
</Button>
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
<Button
icon={RefreshCw}
color="primary"
onClick={() => {}}
data-cy="button"
>
Restart
</Button>
</ButtonGroup>

View file

@ -14,7 +14,11 @@ function Template({
children,
}: JSX.IntrinsicAttributes & PropsWithChildren<Props>) {
return (
<CopyButton copyText={copyText} displayText={displayText}>
<CopyButton
copyText={copyText}
displayText={displayText}
data-cy="copy-button"
>
{children}
</CopyButton>
);

View file

@ -5,7 +5,9 @@ import { CopyButton } from './CopyButton';
test('should display a CopyButton with children', async () => {
const children = 'test button children';
const { findByText } = render(
<CopyButton copyText="">{children}</CopyButton>
<CopyButton copyText="" data-cy="copy-button">
{children}
</CopyButton>
);
const button = await findByText(children);
@ -25,7 +27,9 @@ test('CopyButton should copy text to clipboard', async () => {
const children = 'button';
const copyText = 'text successfully copied to clipboard';
const { findByText } = render(
<CopyButton copyText={copyText}>{children}</CopyButton>
<CopyButton copyText={copyText} data-cy="copy-button">
{children}
</CopyButton>
);
const button = await findByText(children);

View file

@ -2,6 +2,8 @@ import { ComponentProps, PropsWithChildren } from 'react';
import clsx from 'clsx';
import { Check, Copy } from 'lucide-react';
import { AutomationTestingProps } from '@/types';
import { Icon } from '@@/Icon';
import { Button } from '../Button';
@ -9,7 +11,7 @@ import { Button } from '../Button';
import styles from './CopyButton.module.css';
import { useCopy } from './useCopy';
export interface Props {
export interface Props extends AutomationTestingProps {
copyText: string;
fadeDelay?: number;
displayText?: string;
@ -26,6 +28,7 @@ export function CopyButton({
color,
indicatorPosition = 'right',
children,
'data-cy': dataCy,
}: PropsWithChildren<Props>) {
const { handleCopy, copiedSuccessfully } = useCopy(copyText, fadeDelay);
@ -57,6 +60,7 @@ export function CopyButton({
type="button"
icon={Copy}
disabled={!copyText}
data-cy={dataCy}
>
{children}
</Button>

View file

@ -23,8 +23,8 @@ type ConfirmOrClick =
export function DeleteButton({
disabled,
size,
'data-cy': dataCy,
children,
'data-cy': dataCy,
...props
}: PropsWithChildren<
AutomationTestingProps &

View file

@ -17,6 +17,7 @@ function Template({ loadingText, isLoading }: Args) {
return (
<LoadingButton
loadingText={loadingText}
data-cy="loading-button"
isLoading={isLoading}
icon={Download}
>
@ -34,7 +35,12 @@ export const Example = Template.bind({});
export function IsLoading() {
return (
<LoadingButton loadingText="loading" isLoading icon={Download}>
<LoadingButton
loadingText="loading"
isLoading
icon={Download}
data-cy="loading-button"
>
Download
</LoadingButton>
);

View file

@ -7,7 +7,7 @@ test('when isLoading is true should show spinner and loading text', async () =>
const children = 'not visible';
const { queryByText, findByText, container } = render(
<LoadingButton loadingText={loadingText} isLoading>
<LoadingButton loadingText={loadingText} isLoading data-cy="loading-button">
{children}
</LoadingButton>
);
@ -27,7 +27,11 @@ test('should show children when false', async () => {
const children = 'visible';
const { queryByText, container } = render(
<LoadingButton loadingText={loadingText} isLoading={false}>
<LoadingButton
loadingText={loadingText}
isLoading={false}
data-cy="loading-button"
>
{children}
</LoadingButton>
);

View file

@ -47,6 +47,7 @@ export function ColumnVisibilityMenu<D extends object>({
<div key={column.id}>
<Checkbox
checked={column.getIsVisible()}
data-cy="column-visibility-checkbox"
label={
typeof column.columnDef.header === 'string'
? column.columnDef.header

View file

@ -108,7 +108,8 @@ export function Datatable<D extends DefaultType>({
);
const allColumns = useMemo(
() => _.compact([!disableSelect && createSelectColumn<D>(), ...columns]),
() =>
_.compact([!disableSelect && createSelectColumn<D>(dataCy), ...columns]),
[disableSelect, columns]
);
@ -179,6 +180,7 @@ export function Datatable<D extends DefaultType>({
description={description}
renderTableActions={() => renderTableActions(selectedItems)}
renderTableSettings={() => renderTableSettings(tableInstance)}
data-cy={`${dataCy}-header`}
/>
<DatatableContent<D>

View file

@ -1,5 +1,7 @@
import { ReactNode } from 'react';
import { AutomationTestingProps } from '@/types';
import { IconProps } from '@@/Icon';
import { SearchBar } from './SearchBar';
@ -14,7 +16,7 @@ type Props = {
renderTableActions?(): ReactNode;
description?: ReactNode;
titleId?: string;
};
} & AutomationTestingProps;
export function DatatableHeader({
onSearchChange,
@ -25,12 +27,19 @@ export function DatatableHeader({
titleIcon,
description,
titleId,
'data-cy': dataCy,
}: Props) {
if (!title) {
return null;
}
const searchBar = <SearchBar value={searchValue} onChange={onSearchChange} />;
const searchBar = (
<SearchBar
value={searchValue}
onChange={onSearchChange}
data-cy={`${dataCy}-search-input`}
/>
);
const tableActions = !!renderTableActions && (
<Table.Actions>{renderTableActions()}</Table.Actions>
);
@ -44,6 +53,7 @@ export function DatatableHeader({
label={title}
icon={titleIcon}
description={description}
data-cy={dataCy}
>
{searchBar}
{tableActions}

View file

@ -48,6 +48,7 @@ export function MultipleSelectionFilter({
type="checkbox"
checked={value.includes(option)}
onChange={() => handleChange(option)}
data-cy={`filter_${filterKey}_${index}`}
/>
<label htmlFor={`filter_${filterKey}_${index}`}>
{option}

View file

@ -8,13 +8,15 @@ import {
useReactTable,
} from '@tanstack/react-table';
import { AutomationTestingProps } from '@/types';
import { defaultGetRowId } from './defaultGetRowId';
import { Table } from './Table';
import { NestedTable } from './NestedTable';
import { DatatableContent } from './DatatableContent';
import { BasicTableSettings, DefaultType } from './types';
interface Props<D extends DefaultType> {
interface Props<D extends DefaultType> extends AutomationTestingProps {
dataset: D[];
columns: TableOptions<D>['columns'];
@ -41,6 +43,7 @@ export function NestedDatatable<D extends DefaultType>({
isLoading,
initialSortBy,
search,
'data-cy': dataCy,
'aria-label': ariaLabel,
}: Props<D>) {
const tableInstance = useReactTable<D>({
@ -74,6 +77,7 @@ export function NestedDatatable<D extends DefaultType>({
emptyContentLabel={emptyContentLabel}
renderRow={(row) => <Table.Row<D> cells={row.getVisibleCells()} />}
aria-label={ariaLabel}
data-cy={dataCy}
/>
</Table.Container>
</NestedTable>

View file

@ -25,6 +25,7 @@ export function QuickActionsSettings({ actions }: Props) {
{actions.map(({ id, label }) => (
<Checkbox
key={id}
data-cy="quick-actions-checkbox"
label={label}
id={`quick-actions-${id}`}
checked={!settings.hiddenQuickActions.includes(id)}

View file

@ -37,15 +37,21 @@ export function SearchBar({
<Search className="searchIcon lucide shrink-0" />
<input
type="text"
data-cy={dataCy}
className="searchInput"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder={placeholder}
data-cy={dataCy}
aria-label="Search input"
/>
{children}
<Button onClick={onClear} icon={X} color="none" disabled={!searchValue} />
<Button
onClick={onClear}
icon={X}
color="none"
disabled={!searchValue}
data-cy={`${dataCy}-clear-button`}
/>
</div>
);
}

View file

@ -21,6 +21,7 @@ export function TableSettingsMenuAutoRefresh({ onChange, value }: Props) {
<>
<Checkbox
id="settings-auto-refresh"
data-cy="settings-auto-refresh"
label="Auto refresh"
checked={isEnabled}
onChange={(e) => onChange(e.target.checked ? 10 : 0)}
@ -30,6 +31,7 @@ export function TableSettingsMenuAutoRefresh({ onChange, value }: Props) {
<div>
<label htmlFor="settings_refresh_rate">Refresh rate</label>
<select
data-cy="settings-refresh-rate"
id="settings_refresh_rate"
className="small-select"
value={value}

View file

@ -8,6 +8,7 @@ import { defaultGetRowId } from './defaultGetRowId';
export function buildNameColumn<T extends DefaultType>(
nameKey: keyof T,
path: string,
dataCy: string,
idParam = 'id',
idGetter: (row: T) => string = defaultGetRowId<T>
): ColumnDef<T> {
@ -35,6 +36,7 @@ export function buildNameColumn<T extends DefaultType>(
to={path}
params={{ [idParam]: idGetter(row.original) }}
title={name}
data-cy={`${dataCy}_${name}`}
>
{name}
</Link>

View file

@ -18,6 +18,7 @@ export function buildExpandColumn<T extends DefaultType>(): ColumnDef<T> {
color="none"
icon={table.getIsAllRowsExpanded() ? ChevronDown : ChevronUp}
title="Expand all"
data-cy="expand-all-rows-button"
aria-label="Expand all rows"
/>
)
@ -35,6 +36,7 @@ export function buildExpandColumn<T extends DefaultType>(): ColumnDef<T> {
color="none"
icon={row.getIsExpanded() ? ChevronDown : ChevronUp}
title={row.getIsExpanded() ? 'Collapse' : 'Expand'}
data-cy={`expand-row-button_${row.index}`}
aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
aria-expanded={row.getIsExpanded()}
/>

View file

@ -2,7 +2,7 @@ import { ColumnDef, Row } from '@tanstack/react-table';
import { Checkbox } from '@@/form-components/Checkbox';
export function createSelectColumn<T>(): ColumnDef<T> {
export function createSelectColumn<T>(dataCy: string): ColumnDef<T> {
let lastSelectedId = '';
return {
@ -10,6 +10,7 @@ export function createSelectColumn<T>(): ColumnDef<T> {
header: ({ table }) => (
<Checkbox
id="select-all"
data-cy={`select-all-checkbox-${dataCy}`}
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
@ -24,6 +25,7 @@ export function createSelectColumn<T>(): ColumnDef<T> {
cell: ({ row, table }) => (
<Checkbox
id={`select-row-${row.id}`}
data-cy={`select-row-checkbox_${row.id}`}
checked={row.getIsSelected()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}

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}

View file

@ -69,6 +69,7 @@ export function Dialog<T>({
color={button.color}
key={index}
size="medium"
data-cy={button.dataCy}
>
{button.label} {button.timeout && count ? `(${count})` : null}
</Button>

View file

@ -1,5 +1,7 @@
import { ReactNode, useState } from 'react';
import { AutomationTestingProps } from '@/types';
import { SwitchField } from '@@/form-components/SwitchField';
import { ModalType, type ButtonOptions } from './types';
@ -16,6 +18,7 @@ function SwitchPrompt({
modalType,
message,
defaultValue = false,
'data-cy': dataCy,
}: {
onSubmit: OnSubmit<{ value: boolean }>;
title: string;
@ -24,7 +27,7 @@ function SwitchPrompt({
modalType?: ModalType;
message?: ReactNode;
defaultValue?: boolean;
}) {
} & AutomationTestingProps) {
const [value, setValue] = useState(defaultValue);
return (
@ -36,6 +39,7 @@ function SwitchPrompt({
{message && <div className="mb-3">{message}</div>}
<SwitchField
name="value"
data-cy={dataCy}
label={switchLabel}
checked={value}
onChange={setValue}
@ -56,12 +60,15 @@ export async function openSwitchPrompt(
modalType,
message,
defaultValue,
'data-cy': dataCy,
}: {
confirmButton?: ButtonOptions<true>;
modalType?: ModalType;
message?: ReactNode;
defaultValue?: boolean;
} = {}
} & AutomationTestingProps = {
'data-cy': 'switch-prompt',
}
) {
return openModal(SwitchPrompt, {
confirmButton,
@ -70,5 +77,6 @@ export async function openSwitchPrompt(
modalType,
message,
defaultValue,
'data-cy': dataCy,
});
}

View file

@ -8,6 +8,7 @@ export interface ButtonOptions<TValue = undefined> {
color?: ComponentProps<typeof Button>['color'];
value?: TValue;
timeout?: number;
dataCy: string;
}
export interface ButtonsOptions<T> {

View file

@ -7,15 +7,20 @@ import { ButtonOptions } from './types';
export function buildConfirmButton(
label = 'Confirm',
color: ComponentProps<typeof Button>['color'] = 'primary',
timeout = 0
timeout = 0,
dataCy = 'modal-confirm-button'
): ButtonOptions<true> {
return { label, color, value: true, timeout };
return { label, color, value: true, timeout, dataCy };
}
export function buildCancelButton(label = 'Cancel'): ButtonOptions<false> {
export function buildCancelButton(
label = 'Cancel',
dataCy = 'modal-cancel-button'
): ButtonOptions<false> {
return {
label,
color: 'default',
value: false,
dataCy,
};
}