Skip to content
Open
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
181 changes: 181 additions & 0 deletions packages/shared/src/components/modals/post/CommentModal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { type ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import CommentModal from './CommentModal';
import Post from '../../../../__tests__/fixture/post';
import loggedUser from '../../../../__tests__/fixture/loggedUser';
import { useAuthContext } from '../../../contexts/AuthContext';
import useCommentById from '../../../hooks/comments/useCommentById';
import { useNotificationToggle } from '../../../hooks/notifications';
import { useMutateComment } from '../../../hooks/post/useMutateComment';

jest.mock('../common/Modal', () => {
const mockReact = jest.requireActual('react') as typeof React;

const MockModalBody = mockReact.forwardRef<
HTMLElement,
{ children: ReactNode; className?: string }
>(({ children, className }, ref) => (
<section className={className} data-testid="modal-body" ref={ref}>
{children}
</section>
));

MockModalBody.displayName = 'MockModalBody';

const Modal = ({ children }: { children: ReactNode }) => (
<div data-testid="modal">{children}</div>
);

Modal.Body = MockModalBody;

return { Modal };
});

jest.mock('../../comments/CommentContainer', () => ({
__esModule: true,
default: () => <div data-testid="comment-container" />,
}));

jest.mock('../../fields/Switch', () => {
const mockReact = jest.requireActual('react') as typeof React;

const MockSwitch = mockReact.forwardRef<
HTMLLabelElement,
{
children: ReactNode;
className?: string;
labelClassName?: string;
}
>(({ children, className, labelClassName }, ref) => (
<label
className={className}
data-testid="notification-switch"
htmlFor="notification-switch-input"
ref={ref}
>
<span className={labelClassName}>{children}</span>
<input id="notification-switch-input" type="checkbox" />
</label>
));

MockSwitch.displayName = 'MockSwitch';

return {
Switch: MockSwitch,
};
});

jest.mock('../../fields/MarkdownInput/CommentMarkdownInput', () => {
const mockReact = jest.requireActual('react') as typeof React;

const MockCommentMarkdownInput = mockReact.forwardRef<
HTMLFormElement,
{
className?: Record<string, string>;
formProps?: Record<string, string>;
}
>(({ className, formProps }, ref) => (
<form
{...formProps}
className={className?.container}
data-testid="comment-form"
ref={ref}
>
<div
className={className?.markdownContainer}
data-testid="markdown-container"
>
<textarea
aria-label="Share your thoughts"
className={className?.input}
/>
</div>
</form>
));

MockCommentMarkdownInput.displayName = 'MockCommentMarkdownInput';

return {
CommentMarkdownInput: MockCommentMarkdownInput,
};
});

jest.mock('../../../contexts/AuthContext', () => ({
useAuthContext: jest.fn(),
}));

jest.mock('../../../hooks/post/useMutateComment', () => ({
useMutateComment: jest.fn(),
}));

jest.mock('../../../hooks/notifications', () => ({
useNotificationToggle: jest.fn(),
}));

jest.mock('../../../hooks/comments/useCommentById', () => ({
__esModule: true,
default: jest.fn(),
}));

const mockUseAuthContext = useAuthContext as jest.Mock;
const mockUseMutateComment = useMutateComment as jest.Mock;
const mockUseNotificationToggle = useNotificationToggle as jest.Mock;
const mockUseCommentById = useCommentById as jest.Mock;

const defaultProps = {
isOpen: true,
onRequestClose: jest.fn(),
post: Post,
};

const renderComponent = () => {
const client = new QueryClient();

return render(
<QueryClientProvider client={client}>
<CommentModal {...defaultProps} />
</QueryClientProvider>,
);
};

describe('CommentModal', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseAuthContext.mockReturnValue({ user: loggedUser });
mockUseMutateComment.mockReturnValue({
mutateComment: jest.fn(),
isLoading: false,
isSuccess: false,
});
mockUseNotificationToggle.mockReturnValue({
shouldShowCta: true,
isEnabled: true,
onToggle: jest.fn(),
onSubmitted: jest.fn(),
});
mockUseCommentById.mockReturnValue({ comment: null });
});

it('should keep the mobile modal background on the scroll surface', () => {
renderComponent();

expect(screen.getByTestId('modal-body')).toHaveClass(
'bg-background-default',
);
});

it('should keep the editor and notification switch in the normal flex flow', () => {
renderComponent();

const commentForm = screen.getByTestId('comment-form');

expect(commentForm).toHaveClass('flex', 'min-h-0');
expect(screen.getByTestId('markdown-container')).toHaveClass(
'min-h-0',
'flex-1',
);
expect(screen.getByTestId('notification-switch')).toBeInTheDocument();
expect(commentForm).not.toHaveAttribute('style');
});
});
46 changes: 7 additions & 39 deletions packages/shared/src/components/modals/post/CommentModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import type { ReactElement } from 'react';
import React, {
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import type { QueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import classNames from 'classnames';
Expand All @@ -15,7 +9,6 @@ import { FormWrapper } from '../../fields/form';
import type { CommentMarkdownInputProps } from '../../fields/MarkdownInput/CommentMarkdownInput';
import { CommentMarkdownInput } from '../../fields/MarkdownInput/CommentMarkdownInput';
import { useMutateComment } from '../../../hooks/post/useMutateComment';
import { useVisualViewport } from '../../../hooks/utils/useVisualViewport';
import type { Comment, PostCommentsData } from '../../../graphql/comments';
import { useNotificationToggle } from '../../../hooks/notifications';
import { NotificationPromptSource } from '../../../lib/log';
Expand Down Expand Up @@ -79,7 +72,6 @@ const getCommentFromCache = ({

return undefined;
};

export interface CommentModalProps
extends LazyModalCommonProps,
CommentMarkdownInputProps {
Expand All @@ -97,11 +89,6 @@ export default function CommentModal({
post,
initialContent: initialContentFromProps,
}: CommentModalProps): ReactElement {
const inputRef = useRef<HTMLFormElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const replyRef = useRef<HTMLDivElement>(null);
const switchRef = useRef<HTMLLabelElement>(null);

const { user } = useAuthContext();
const client = useQueryClient();
const [modalNode, setModalNode] = useState<HTMLElement>(null);
Expand Down Expand Up @@ -166,20 +153,6 @@ export default function CommentModal({
modalNode?.scrollTo?.({ behavior: 'auto', top: 10000 });
}, [modalNode]);

const { height } = useVisualViewport();
const replyHeight = replyRef.current?.clientHeight ?? 0;
const footerHeight = switchRef.current?.clientHeight ?? 0;
const headerHeight = headerRef.current?.offsetHeight ?? 0;
const totalHeight = height - headerHeight - replyHeight - footerHeight;
const inputHeight = totalHeight > 0 ? Math.max(totalHeight, 300) : 'auto';

if (
inputRef.current &&
inputRef.current.style?.height !== `${inputHeight}px`
) {
inputRef.current.style.height = `${inputHeight}px`;
}

const { submitCopy, initialContent } = useMemo(() => {
if (isEdit) {
return {
Expand Down Expand Up @@ -217,7 +190,7 @@ export default function CommentModal({
className="!border-none"
overlayClassName="!touch-none"
>
<Modal.Body className="!p-0" ref={refCallback}>
<Modal.Body className="bg-background-default !p-0" ref={refCallback}>
<WriteCommentContext.Provider
value={{ mutateComment: mutateCommentResult }}
>
Expand All @@ -235,10 +208,10 @@ export default function CommentModal({
disabled: isSuccess,
}}
className={{
container: 'flex-1 first:!border-none',
container:
'flex min-h-full flex-1 flex-col bg-background-default first:!border-none',
header: 'sticky top-0 z-2 w-full bg-background-default',
}}
headerRef={headerRef}
>
{isReply && comment && (
<>
Expand All @@ -251,10 +224,7 @@ export default function CommentModal({
postAuthorId={post?.author?.id}
postScoutId={post?.scout?.id}
/>
<div
className="ml-12 flex gap-2 border-l border-border-subtlest-tertiary py-3 pl-5 text-text-tertiary typo-caption1"
ref={replyRef}
>
<div className="ml-12 flex gap-2 border-l border-border-subtlest-tertiary py-3 pl-5 text-text-tertiary typo-caption1">
Reply to
<span className="font-bold text-text-primary">
{comment.author?.username}
Expand All @@ -263,16 +233,15 @@ export default function CommentModal({
</>
)}
<CommentMarkdownInput
ref={inputRef}
replyTo={null}
post={post}
parentCommentId={parentCommentId}
editCommentId={isEdit && editCommentId}
initialContent={initialContent}
className={{
markdownContainer: 'flex-1',
markdownContainer: 'min-h-0 flex-1',
container: classNames(
'flex flex-col',
'flex min-h-0 flex-col',
isReply ? 'h-[calc(100%-2.5rem)]' : 'h-full',
),
tab: 'flex-1',
Expand All @@ -291,7 +260,6 @@ export default function CommentModal({
compact={false}
checked={isEnabled}
onToggle={onToggle}
ref={switchRef}
>
Receive updates when other members engage
</Switch>
Expand Down
Loading