Wrote end-to-end cypress tests

This commit is contained in:
ireic
2020-01-05 02:54:46 +01:00
parent ad74afb628
commit 64b237e046
60 changed files with 3698 additions and 215 deletions

View File

@@ -25,7 +25,7 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilte
const areFiltersCleared = !searchTerm && userIds.length === 0 && !myOnly && !recent;
return (
<Filters>
<Filters data-testid="board-filters">
<SearchInput
icon="search"
value={searchTerm}

View File

@@ -25,6 +25,7 @@ const ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, project
variant="empty"
dropdownWidth={343}
placeholder="Unassigned"
name="assignees"
value={issue.userIds}
options={userOptions}
onChange={userIds => {
@@ -41,6 +42,7 @@ const ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, project
variant="empty"
dropdownWidth={343}
withClearValue={false}
name="reporter"
value={issue.reporterId}
options={userOptions}
onChange={userId => updateIssue({ reporterId: userId })}

View File

@@ -50,7 +50,7 @@ const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
};
return (
<Comment>
<Comment data-testid="issue-comment">
<UserAvatar name={comment.user.name} avatarUrl={comment.user.avatarUrl} />
<Content>
<Username>{comment.user.name}</Username>
@@ -71,7 +71,7 @@ const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
<ConfirmModal
title="Are you sure you want to delete this comment?"
message="Once you delete, it's gone for good."
confirmText="Delete Comment"
confirmText="Delete comment"
onConfirm={handleCommentDelete}
renderLink={modal => <DeleteLink onClick={modal.open}>Delete</DeleteLink>}
/>

View File

@@ -28,6 +28,7 @@ const ProjectBoardIssueDetailsEstimateTracking = ({ issue, updateIssue }) => (
<SectionTitle>Time Tracking</SectionTitle>
<Modal
testid="modal:tracking"
width={400}
renderLink={modal => (
<TrackingLink onClick={modal.open}>

View File

@@ -19,6 +19,7 @@ const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => (
variant="empty"
withClearValue={false}
dropdownWidth={343}
name="priority"
value={issue.priority}
options={Object.values(IssuePriority).map(priority => ({
value: priority,

View File

@@ -19,6 +19,7 @@ const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
variant="empty"
dropdownWidth={343}
withClearValue={false}
name="status"
value={issue.status}
options={Object.values(IssueStatus).map(status => ({
value: status,

View File

@@ -16,6 +16,7 @@ const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
variant="empty"
dropdownWidth={150}
withClearValue={false}
name="type"
value={issue.type}
options={Object.values(IssueType).map(type => ({
value: type,

View File

@@ -24,6 +24,7 @@ const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
<IssueLink
to={`${match.url}/issues/${issue.id}`}
ref={provided.innerRef}
data-testid="list-issue"
{...provided.draggableProps}
{...provided.dragHandleProps}
>

View File

@@ -31,7 +31,11 @@ const ProjectBoardList = ({ status, project, filters }) => {
{`${IssueStatusCopy[status]} `}
<IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount>
</Title>
<Issues {...provided.droppableProps} ref={provided.innerRef}>
<Issues
{...provided.droppableProps}
ref={provided.innerRef}
data-testid={`board-list:${status}`}
>
{filteredListIssues.map((issue, index) => (
<Issue key={issue.id} projectUsers={project.users} issue={issue} index={index} />
))}

View File

@@ -49,6 +49,7 @@ const ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => {
render={routeProps => (
<Modal
isOpen
testid="modal:issue-details"
width={1040}
withCloseIcon={false}
onClose={() => history.push(match.url)}

View File

@@ -30,7 +30,7 @@ const propTypes = {
modalClose: PropTypes.func.isRequired,
};
const ProjectIssueCreateForm = ({ project, fetchProject, onCreate, modalClose }) => {
const ProjectIssueCreate = ({ project, fetchProject, onCreate, modalClose }) => {
const [{ isCreating }, createIssue] = useApi.post('/issues');
const { currentUserId } = useCurrentUser();
@@ -168,6 +168,6 @@ const renderUser = project => ({ value: userId, removeOptionValue }) => {
);
};
ProjectIssueCreateForm.propTypes = propTypes;
ProjectIssueCreate.propTypes = propTypes;
export default ProjectIssueCreateForm;
export default ProjectIssueCreate;

View File

@@ -9,10 +9,11 @@ export const FormCont = styled.div`
`;
export const FormElement = styled(Form.Element)`
width: 100%;
max-width: 640px;
`;
export const FormHeading = styled.div`
export const FormHeading = styled.h1`
padding: 6px 0 15px;
${font.size(24)}
${font.medium}

View File

@@ -10,7 +10,7 @@ import NavbarLeft from './NavbarLeft';
import Sidebar from './Sidebar';
import Board from './Board';
import IssueSearch from './IssueSearch';
import IssueCreateForm from './IssueCreateForm';
import IssueCreate from './IssueCreate';
import ProjectSettings from './ProjectSettings';
import { ProjectPage } from './Styles';
@@ -49,6 +49,7 @@ const Project = () => {
{issueSearchModalHelpers.isOpen() && (
<Modal
isOpen
testid="modal:issue-search"
variant="aside"
width={600}
onClose={issueSearchModalHelpers.close}
@@ -59,11 +60,12 @@ const Project = () => {
{issueCreateModalHelpers.isOpen() && (
<Modal
isOpen
testid="modal:issue-create"
width={800}
withCloseIcon={false}
onClose={issueCreateModalHelpers.close}
renderContent={modal => (
<IssueCreateForm
<IssueCreate
project={project}
fetchProject={fetchProject}
onCreate={() => history.push(`${match.url}/board`)}

View File

@@ -8,6 +8,7 @@ import App from 'App';
ReactDOM.render(<App />, document.getElementById('root'));
// QUERY component cache-only doesn't work until first req finishes, look at currentUser on page load
// APP IS NOT RESPONSIVE - REDUCE BROWSER HEIGHT, ISSUES DONT SCROLL
// TODO: UPDATE FORMIK TO FIX SETFIELDVALUE TO EMPTY ARRAY ISSUE https://github.com/jaredpalmer/formik/pull/2144
// REFACTOR HTML TO USE SEMANTIC ELEMENTS

View File

@@ -18,12 +18,19 @@ const defaultProps = {
};
const Avatar = ({ className, avatarUrl, name, size, ...otherProps }) => {
const sharedProps = {
className,
size,
'data-testid': name ? `avatar:${name}` : 'avatar',
...otherProps,
};
if (avatarUrl) {
return <Image className={className} size={size} avatarUrl={avatarUrl} {...otherProps} />;
return <Image avatarUrl={avatarUrl} {...sharedProps} />;
}
return (
<Letter className={className} size={size} color={getColorFromName(name)} {...otherProps}>
<Letter color={getColorFromName(name)} {...sharedProps}>
<span>{name.charAt(0)}</span>
</Letter>
);

View File

@@ -47,8 +47,9 @@ const ConfirmModal = ({
return (
<StyledConfirmModal
withCloseIcon={false}
className={className}
testid="modal:confirm"
withCloseIcon={false}
renderLink={renderLink}
renderContent={modal => (
<>

View File

@@ -15,23 +15,29 @@ const propTypes = {
label: PropTypes.string,
tip: PropTypes.string,
error: PropTypes.string,
name: PropTypes.string,
};
const defaultProps = {
className: undefined,
label: null,
tip: null,
error: null,
label: undefined,
tip: undefined,
error: undefined,
name: undefined,
};
const generateField = FormComponent => {
const FieldComponent = ({ className, label, tip, error, ...otherProps }) => {
const FieldComponent = ({ className, label, tip, error, name, ...otherProps }) => {
const fieldId = uniqueId('form-field-');
return (
<StyledField className={className} hasLabel={!!label}>
<StyledField
className={className}
hasLabel={!!label}
data-testid={name ? `form-field:${name}` : 'form-field'}
>
{label && <FieldLabel htmlFor={fieldId}>{label}</FieldLabel>}
<FormComponent id={fieldId} invalid={!!error} {...otherProps} />
<FormComponent id={fieldId} invalid={!!error} name={name} {...otherProps} />
{tip && <FieldTip>{tip}</FieldTip>}
{error && <FieldError>{error}</FieldError>}
</StyledField>

View File

@@ -53,7 +53,9 @@ const defaultProps = {
top: 0,
};
const Icon = ({ type, ...iconProps }) => <StyledIcon {...iconProps} code={fontIconCodes[type]} />;
const Icon = ({ type, ...iconProps }) => (
<StyledIcon {...iconProps} data-testid={`icon:${type}`} code={fontIconCodes[type]} />
);
Icon.propTypes = propTypes;
Icon.defaultProps = defaultProps;

View File

@@ -9,6 +9,7 @@ import { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Style
const propTypes = {
className: PropTypes.string,
testid: PropTypes.string,
variant: PropTypes.oneOf(['center', 'aside']),
width: PropTypes.number,
withCloseIcon: PropTypes.bool,
@@ -20,6 +21,7 @@ const propTypes = {
const defaultProps = {
className: undefined,
testid: 'modal',
variant: 'center',
width: 600,
withCloseIcon: true,
@@ -30,6 +32,7 @@ const defaultProps = {
const Modal = ({
className,
testid,
variant,
width,
withCloseIcon,
@@ -63,9 +66,16 @@ const Modal = ({
{isOpen &&
ReactDOM.createPortal(
<ScrollOverlay data-jira-modal="true">
<ScrollOverlay>
<ClickableOverlay variant={variant} ref={$clickableOverlayRef}>
<StyledModal className={className} variant={variant} width={width} ref={$modalRef}>
<StyledModal
className={className}
variant={variant}
width={width}
data-jira-modal="true"
data-testid={testid}
ref={$modalRef}
>
{withCloseIcon && <CloseIcon type="close" variant={variant} onClick={closeModal} />}
{renderContent({ close: closeModal })}
</StyledModal>

View File

@@ -192,6 +192,7 @@ const SelectDropdown = ({
<Option
key={option.value}
data-select-option-value={option.value}
data-testid={`select-option:${option.label}`}
onMouseEnter={handleOptionMouseEnter}
onClick={() => selectOptionValue(option.value)}
>

View File

@@ -15,6 +15,10 @@ export const StyledSelect = styled.div`
width: 100%;
border: 1px solid ${color.borderLightest};
background: ${color.backgroundLightest};
transition: background 0.1s;
&:hover {
background: ${color.backgroundLight};
}
`}
&:focus {
outline: none;

View File

@@ -20,6 +20,7 @@ const propTypes = {
className: PropTypes.string,
variant: PropTypes.oneOf(['normal', 'empty']),
dropdownWidth: PropTypes.number,
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
defaultValue: PropTypes.any,
placeholder: PropTypes.string,
@@ -37,6 +38,7 @@ const defaultProps = {
className: undefined,
variant: 'normal',
dropdownWidth: undefined,
name: undefined,
value: undefined,
defaultValue: undefined,
placeholder: 'Select',
@@ -52,6 +54,7 @@ const Select = ({
className,
variant,
dropdownWidth,
name,
value: propsValue,
defaultValue,
placeholder,
@@ -140,7 +143,11 @@ const Select = ({
onKeyDown={handleFocusedSelectKeydown}
invalid={invalid}
>
<ValueContainer variant={variant} onClick={activateDropdown}>
<ValueContainer
variant={variant}
data-testid={name ? `select:${name}` : 'select'}
onClick={activateDropdown}
>
{isValueEmpty && <Placeholder>{placeholder}</Placeholder>}
{!isValueEmpty && !isMulti && propsRenderValue

View File

@@ -1,8 +1,11 @@
export const KeyCodes = {
ESCAPE: 27,
TAB: 9,
ENTER: 13,
ESCAPE: 27,
SPACE: 32,
ARROW_LEFT: 37,
ARROW_UP: 38,
ARROW_RIGHT: 39,
ARROW_DOWN: 40,
M: 77,
};

View File

@@ -12,7 +12,6 @@ const useQuery = (url, propsVariables = {}, options = {}) => {
data: null,
error: null,
isLoading: !lazy,
wasCalled: !lazy,
variables: {},
});

View File

@@ -4,7 +4,7 @@ import useDeepCompareMemoize from 'shared/hooks/deepCompareMemoize';
const useOnOutsideClick = (
$ignoredElementRefs,
shouldListen,
isListening,
onOutsideClick,
$listeningElementRef,
) => {
@@ -17,19 +17,19 @@ const useOnOutsideClick = (
};
const handleMouseUp = event => {
const isAnyIgnoredElementParentOfTarget = $ignoredElementRefsMemoized.some(
const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(
$elementRef =>
$elementRef.current.contains($mouseDownTargetRef.current) ||
$elementRef.current.contains(event.target),
);
if (event.button === 0 && !isAnyIgnoredElementParentOfTarget) {
if (event.button === 0 && !isAnyIgnoredElementAncestorOfTarget) {
onOutsideClick();
}
};
const $listeningElement = ($listeningElementRef || {}).current || document;
if (shouldListen) {
if (isListening) {
$listeningElement.addEventListener('mousedown', handleMouseDown);
$listeningElement.addEventListener('mouseup', handleMouseUp);
}
@@ -38,7 +38,7 @@ const useOnOutsideClick = (
$listeningElement.removeEventListener('mousedown', handleMouseDown);
$listeningElement.removeEventListener('mouseup', handleMouseUp);
};
}, [$ignoredElementRefsMemoized, $listeningElementRef, shouldListen, onOutsideClick]);
}, [$ignoredElementRefsMemoized, $listeningElementRef, isListening, onOutsideClick]);
};
export default useOnOutsideClick;