Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion webpack/ForemanWebhooks/Routes/ForemanWebhooksRoutes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import ConnectedWebhooksIndexPage from './Webhooks/WebhooksIndexPage';
import ConnectedWebhooksIndexPage from './Webhooks/WebhooksIndexPage/WebhooksIndexPage';

const ForemanWebhooksRoutes = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import actionCellFormatter from '../actionCellFormatter';

jest.mock(
'foremanReact/components/common/table',
() => ({
cellFormatter: content => (
<div data-testid="cell-formatter-mock">
{content === false ? null : content}
</div>
),
}),
{ virtual: true }
);

jest.mock('../../ActionButtons/ActionButton', () => {
// eslint-disable-next-line global-require
const PropTypes = require('prop-types');

const ActionButton = ({ id, name, canDelete }) => (
<span
data-testid="action-button"
data-id={id}
data-name={name}
data-can-delete={String(canDelete)}
/>
);
ActionButton.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
name: PropTypes.string.isRequired,
canDelete: PropTypes.bool,
};
ActionButton.defaultProps = {
canDelete: false,
};

return { ActionButton };
});

describe('actionCellFormatter', () => {
const webhookActions = {
deleteWebhook: jest.fn(),
testWebhook: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
});

it('wraps the action control in cellFormatter when rowData is present and the row is editable', () => {
const formatter = actionCellFormatter(webhookActions);
const row = { id: 10, name: 'Wh', can_edit: true, can_delete: true };

render(formatter(null, { rowData: row }));

expect(screen.getByTestId('cell-formatter-mock')).toBeInTheDocument();
const action = screen.getByTestId('action-button');
expect(action).toHaveAttribute('data-id', '10');
expect(action).toHaveAttribute('data-name', 'Wh');
expect(action).toHaveAttribute('data-can-delete', 'true');
});

it('passes canDelete from can_delete when rowData is present', () => {
const formatter = actionCellFormatter(webhookActions);
const row = { id: 2, name: 'A', can_edit: true, can_delete: false };

render(formatter(null, { rowData: row }));

expect(screen.getByTestId('action-button')).toHaveAttribute(
'data-can-delete',
'false'
);
});

it('renders falsy content through cellFormatter when rowData is present but the row is not editable', () => {
const formatter = actionCellFormatter(webhookActions);
const row = { id: 5, name: 'B', can_edit: false };

render(formatter(null, { rowData: row }));

expect(screen.getByTestId('cell-formatter-mock')).toBeInTheDocument();
expect(screen.queryByTestId('action-button')).not.toBeInTheDocument();
});

it('returns the action control without cellFormatter when rowData is omitted', () => {
const formatter = actionCellFormatter(webhookActions);
const row = { id: 20, name: 'Direct', can_edit: true, canDelete: true };

render(formatter(row));

expect(screen.queryByTestId('cell-formatter-mock')).not.toBeInTheDocument();
expect(screen.getByTestId('action-button')).toHaveAttribute(
'data-id',
'20'
);
});

it('returns nothing when rowData is omitted and the row is not editable', () => {
const formatter = actionCellFormatter(webhookActions);
const row = { id: 1, name: 'X', can_edit: false };

const { container } = render(formatter(row));

expect(container).toBeEmptyDOMElement();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import enabledCellFormatter from '../enabledCellFormatter';

jest.mock('@patternfly/react-icons', () => ({
CheckIcon: () => <span data-testid="check-icon" />,
BanIcon: () => <span data-testid="ban-icon" />,
}));

const renderCell = (value, extra) => {
const formatter = enabledCellFormatter();
const element = formatter(value, extra);
return render(element);
};

describe('enabledCellFormatter', () => {
it('uses the cell value when row context includes rowData and value is true', () => {
renderCell(true, { rowData: { id: 1, enabled: false } });
expect(screen.getByTestId('check-icon')).toBeInTheDocument();
});

it('uses the cell value when row context includes rowData and value is false', () => {
renderCell(false, { rowData: { id: 1, enabled: true } });
expect(screen.getByTestId('ban-icon')).toBeInTheDocument();
});

it('uses value.enabled when rowData is not present and enabled is true', () => {
renderCell({ enabled: true });
expect(screen.getByTestId('check-icon')).toBeInTheDocument();
});

it('uses value.enabled when rowData is not present and enabled is false', () => {
renderCell({ enabled: false });
expect(screen.getByTestId('ban-icon')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';

import nameToEditFormatter from '../nameToEditFormatter';

jest.mock('@patternfly/react-core', () => {
// eslint-disable-next-line global-require
const PropTypes = require('prop-types');

const Button = ({ children, onClick, isDisabled }) => (
<button type="button" onClick={onClick} disabled={isDisabled}>
{children}
</button>
);
Button.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
isDisabled: PropTypes.bool,
};
Button.defaultProps = {
children: null,
onClick: () => {},
isDisabled: false,
};

return { Button };
});

describe('nameToEditFormatter', () => {
it('renders the cell value as the label and calls the handler with the row id when can_edit is true', () => {
const onEdit = jest.fn();
const formatter = nameToEditFormatter(onEdit);
const row = { id: 42, name: 'Other', can_edit: true };

render(formatter('Shown name', { rowData: row }));

expect(screen.getByText('Shown name')).toBeInTheDocument();
fireEvent.click(screen.getByText('Shown name'));
expect(onEdit).toHaveBeenCalledTimes(1);
expect(onEdit).toHaveBeenCalledWith(42);
});

it('prefers canEdit over can_edit when both are set', () => {
const onEdit = jest.fn();
const formatter = nameToEditFormatter(onEdit);

render(
formatter('Label', {
rowData: { id: 1, name: 'N', canEdit: true, can_edit: false },
})
);

fireEvent.click(screen.getByText('Label'));
expect(onEdit).toHaveBeenCalledWith(1);
});

it('does not call the handler when the row cannot be edited', () => {
const onEdit = jest.fn();
const formatter = nameToEditFormatter(onEdit);

render(
formatter('Read only', {
rowData: { id: 7, name: 'X', can_edit: false },
})
);

const control = screen.getByText('Read only');
expect(control.closest('button')).toBeDisabled();
fireEvent.click(control);
expect(onEdit).not.toHaveBeenCalled();
});

it('uses row.name as the label when rowData is not passed and value is the row object', () => {
const formatter = nameToEditFormatter(jest.fn());
const row = { id: 3, name: 'From row', can_edit: false };

render(formatter(row));

expect(screen.getByText('From row')).toBeInTheDocument();
});
});
Comment thread
Lukshio marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { cellFormatter } from 'foremanReact/components/common/table';
import { ActionButton } from '../ActionButtons/ActionButton';

const actionCellFormatter = webhookActions => (value, extra) => {
const row = extra?.rowData ?? value;
const canEdit = row.canEdit ?? row.can_edit;
const canDelete = row.canDelete ?? row.can_delete;
const { id, name } = row;

const content = canEdit && (
<ActionButton
canDelete={canDelete}
id={id}
name={name}
webhookActions={webhookActions}
/>
);

return extra?.rowData != null ? cellFormatter(content) : content;
};

export default actionCellFormatter;
Comment thread
Lukshio marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ EnabledCell.defaultProps = {
condition: false,
};

export default EnabledCell;
const enabledCellFormatter = () => (value, extra) => {
const condition = extra?.rowData != null ? value : value?.enabled;
return <EnabledCell condition={Boolean(condition)} />;
};

export default enabledCellFormatter;
Comment thread
Lukshio marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Button } from '@patternfly/react-core';

const nameToEditFormatter = onClick => (value, extra) => {
const row = extra?.rowData ?? value;
const canEdit = row.canEdit ?? row.can_edit;
const { id } = row;
const label = extra?.rowData != null ? value : row.name;

return canEdit ? (
<Button
ouiaId="name-edit-active-button"
variant="link"
isInline
component="span"
onClick={() => onClick(id)}
>
{label}
</Button>
) : (
<Button
ouiaId="name-edit-disabled-button"
variant="link"
isInline
isDisabled
component="span"
>
{label}
</Button>
);
};

export default nameToEditFormatter;
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const WebhookCreateModal = ({ onSuccess, onCancel, isOpen }) => {
params: { ...values, controller: 'webhooks' },
successToast: () => __('Webhook was successfully created.'),
handleSuccess: () => {
setIsSubmitting(false);
onSuccess();
},
handleError: () => setIsSubmitting(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const WebhookDeleteModal = ({ toDelete, onSuccess, modalState }) => {

const [isSubmitting, setIsSubmitting] = useState(false);
const dispatch = useDispatch();

const handleSubmit = () => {
setIsSubmitting(true);
dispatch(
Expand All @@ -25,7 +26,10 @@ const WebhookDeleteModal = ({ toDelete, onSuccess, modalState }) => {
errorToast: response =>
// eslint-disable-next-line camelcase
response?.response?.data?.error?.full_messages?.[0] || response,
handleSuccess: onSuccess,
handleSuccess: () => {
setIsSubmitting(false);
onSuccess();
},
handleError: () => setIsSubmitting(false),
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const WebhookEditModal = ({ toEdit, onSuccess, modalState }) => {
params: { ...values, controller: 'webhooks' },
successToast: () => __('Webhook was successfully updated.'),
handleSuccess: () => {
setIsSubmitting(false);
onSuccess();
},
handleError: () => setIsSubmitting(false),
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading