Skip to content

Commit 9540608

Browse files
authored
fix: keep below-threshold label only on cards (#5830)
1 parent ce2d215 commit 9540608

10 files changed

Lines changed: 272 additions & 37 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { useQueryClient } from '@tanstack/react-query';
4+
import { UserVote } from '@dailydotdev/shared/src/graphql/posts';
5+
import type { PostBootData } from '@dailydotdev/shared/src/lib/boot';
6+
import { CompanionEngagements } from './CompanionEngagements';
7+
8+
const mockUseConditionalFeature = jest.fn();
9+
const mockUseAuthContext = jest.fn();
10+
const mockUseQueryClient = useQueryClient as jest.Mock;
11+
12+
jest.mock('@dailydotdev/shared/src/hooks/useConditionalFeature', () => ({
13+
useConditionalFeature: () => mockUseConditionalFeature(),
14+
}));
15+
16+
jest.mock('@dailydotdev/shared/src/contexts/AuthContext', () => ({
17+
useAuthContext: () => mockUseAuthContext(),
18+
}));
19+
20+
jest.mock('@dailydotdev/shared/src/hooks/companion', () => ({
21+
useRawBackgroundRequest: jest.fn(),
22+
}));
23+
24+
jest.mock('@tanstack/react-query', () => ({
25+
...jest.requireActual('@tanstack/react-query'),
26+
useQueryClient: jest.fn(),
27+
}));
28+
29+
describe('CompanionEngagements', () => {
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
mockUseAuthContext.mockReturnValue({ user: { id: 'user-id' } });
33+
mockUseConditionalFeature.mockReturnValue({
34+
value: {
35+
threshold: 3,
36+
belowThresholdLabel: 'New',
37+
newWindowHours: 24,
38+
},
39+
});
40+
mockUseQueryClient.mockReturnValue({
41+
setQueryData: jest.fn(),
42+
});
43+
});
44+
45+
it('does not render below-threshold label in companion', () => {
46+
const post = {
47+
id: 'post-id',
48+
createdAt: new Date().toISOString(),
49+
numUpvotes: 1,
50+
numComments: 2,
51+
userState: {
52+
vote: UserVote.None,
53+
},
54+
} as PostBootData;
55+
56+
render(<CompanionEngagements post={post} />);
57+
58+
expect(screen.queryByText('New')).not.toBeInTheDocument();
59+
});
60+
});

packages/extension/src/companion/CompanionEngagements.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function CompanionEngagements({
4747
const upvotes = post.numUpvotes ?? 0;
4848
const comments = post.numComments ?? 0;
4949
const userHasUpvoted = post.userState?.vote === UserVote.Up;
50-
const { showCount, belowThresholdLabel } = getUpvoteCountDisplay(
50+
const { showCount } = getUpvoteCountDisplay(
5151
upvotes,
5252
upvoteThresholdConfig.threshold,
5353
upvoteThresholdConfig.belowThresholdLabel,
@@ -68,9 +68,6 @@ export function CompanionEngagements({
6868
{upvotes > 1 ? 's' : ''}
6969
</ClickableText>
7070
)}
71-
{!showCount && !!belowThresholdLabel && (
72-
<span>{belowThresholdLabel}</span>
73-
)}
7471
{comments > 0 && (
7572
<span>
7673
{largeNumberFormat(comments)}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import PostMetadata from './PostMetadata';
4+
5+
const mockUseConditionalFeature = jest.fn();
6+
const mockUseAuthContext = jest.fn();
7+
8+
jest.mock('../../../hooks/useConditionalFeature', () => ({
9+
useConditionalFeature: () => mockUseConditionalFeature(),
10+
}));
11+
12+
jest.mock('../../../contexts/AuthContext', () => ({
13+
useAuthContext: () => mockUseAuthContext(),
14+
}));
15+
16+
describe('PostMetadata upvote label visibility', () => {
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
mockUseAuthContext.mockReturnValue({ user: { id: 'user-id' } });
20+
mockUseConditionalFeature.mockReturnValue({
21+
value: {
22+
threshold: 3,
23+
belowThresholdLabel: 'New',
24+
newWindowHours: 24,
25+
},
26+
});
27+
});
28+
29+
it('renders below-threshold label by default', () => {
30+
render(
31+
<PostMetadata
32+
createdAt={new Date().toISOString()}
33+
numUpvotes={1}
34+
readTime={1}
35+
/>,
36+
);
37+
38+
expect(screen.getByText('New')).toBeInTheDocument();
39+
});
40+
41+
it('does not render below-threshold label when disabled for non-card surfaces', () => {
42+
render(
43+
<PostMetadata
44+
createdAt={new Date().toISOString()}
45+
numUpvotes={1}
46+
readTime={1}
47+
showBelowThresholdLabel={false}
48+
/>,
49+
);
50+
51+
expect(screen.queryByText('New')).not.toBeInTheDocument();
52+
});
53+
54+
it('does not render below-threshold label when upvote count is not provided', () => {
55+
render(<PostMetadata createdAt={new Date().toISOString()} readTime={1} />);
56+
57+
expect(screen.queryByText('New')).not.toBeInTheDocument();
58+
});
59+
});

packages/shared/src/components/cards/common/PostMetadata.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface PostMetadataProps
2525
domain?: ReactNode;
2626
pollMetadata?: PollMetadataProps;
2727
userHasUpvoted?: boolean;
28+
showBelowThresholdLabel?: boolean;
2829
}
2930

3031
export default function PostMetadata({
@@ -38,7 +39,9 @@ export default function PostMetadata({
3839
domain,
3940
pollMetadata,
4041
userHasUpvoted = false,
42+
showBelowThresholdLabel = true,
4143
}: PostMetadataProps): ReactElement {
44+
const hasUpvoteCount = typeof numUpvotes === 'number';
4245
const upvoteCount = numUpvotes ?? 0;
4346
const readTimeValue = readTime ?? 0;
4447
const timeActionContent = isVideoType ? 'watch' : 'read';
@@ -94,16 +97,19 @@ export default function PostMetadata({
9497
),
9598
},
9699
!!showReadTime && domain && { key: 'domain', node: domain },
97-
showUpvoteCount && {
98-
key: 'upvotes',
99-
node: (
100-
<span data-testid="numUpvotes">
101-
{largeNumberFormat(upvoteCount)} upvote{upvoteCount > 1 ? 's' : ''}
102-
</span>
103-
),
104-
},
105-
!showUpvoteCount &&
106-
!!upvoteLabel && {
100+
hasUpvoteCount &&
101+
showUpvoteCount && {
102+
key: 'upvotes',
103+
node: (
104+
<span data-testid="numUpvotes">
105+
{largeNumberFormat(upvoteCount)} upvote{upvoteCount > 1 ? 's' : ''}
106+
</span>
107+
),
108+
},
109+
hasUpvoteCount &&
110+
!showUpvoteCount &&
111+
!!upvoteLabel &&
112+
showBelowThresholdLabel && {
107113
key: 'upvotes',
108114
node: <span data-testid="numUpvotes">{upvoteLabel}</span>,
109115
},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import type { Post } from '../../graphql/posts';
4+
import { UserVote } from '../../graphql/posts';
5+
import { RepostListItem } from './RepostListItem';
6+
7+
const mockUseConditionalFeature = jest.fn();
8+
const mockUseAuthContext = jest.fn();
9+
10+
jest.mock('../../hooks/useConditionalFeature', () => ({
11+
useConditionalFeature: () => mockUseConditionalFeature(),
12+
}));
13+
14+
jest.mock('../../contexts/AuthContext', () => ({
15+
useAuthContext: () => mockUseAuthContext(),
16+
}));
17+
18+
describe('RepostListItem', () => {
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
mockUseAuthContext.mockReturnValue({ user: { id: 'user-id' } });
22+
mockUseConditionalFeature.mockReturnValue({
23+
value: {
24+
threshold: 3,
25+
belowThresholdLabel: 'New',
26+
newWindowHours: 24,
27+
},
28+
});
29+
});
30+
31+
it('does not render below-threshold label in repost modal items', () => {
32+
const post = {
33+
id: 'post-id',
34+
title: '',
35+
createdAt: new Date().toISOString(),
36+
numUpvotes: 1,
37+
numComments: 2,
38+
userState: {
39+
vote: UserVote.None,
40+
},
41+
} as Post;
42+
43+
render(<RepostListItem post={post} />);
44+
45+
expect(screen.queryByText('New')).not.toBeInTheDocument();
46+
expect(screen.queryByTestId('repost-upvotes')).not.toBeInTheDocument();
47+
});
48+
});

packages/shared/src/components/modals/RepostListItem.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,14 @@ export function RepostListItem({
3737
const upvotes = post.numUpvotes ?? 0;
3838
const comments = post.numComments ?? 0;
3939
const userHasUpvoted = post.userState?.vote === UserVote.Up;
40-
const { showCount: showUpvotes, belowThresholdLabel: upvoteLabel } =
41-
getUpvoteCountDisplay(
42-
upvotes,
43-
upvoteThresholdConfig.threshold,
44-
upvoteThresholdConfig.belowThresholdLabel,
45-
userHasUpvoted,
46-
post.createdAt,
47-
upvoteThresholdConfig.newWindowHours,
48-
);
40+
const { showCount: showUpvotes } = getUpvoteCountDisplay(
41+
upvotes,
42+
upvoteThresholdConfig.threshold,
43+
upvoteThresholdConfig.belowThresholdLabel,
44+
userHasUpvoted,
45+
post.createdAt,
46+
upvoteThresholdConfig.newWindowHours,
47+
);
4948
const { author } = post;
5049
const showSquadPreview = !isUserSource && !!source;
5150
const isPrivateSquad = !!source && !source.public;
@@ -142,10 +141,15 @@ export function RepostListItem({
142141

143142
{/* Upvotes and comments */}
144143
<div className="mt-3 flex items-center gap-4 text-text-quaternary typo-callout">
145-
<span className="flex items-center gap-1.5">
146-
<UpvoteIcon className="size-4" />
147-
{showUpvotes ? largeNumberFormat(upvotes) : upvoteLabel}
148-
</span>
144+
{showUpvotes && (
145+
<span
146+
className="flex items-center gap-1.5"
147+
data-testid="repost-upvotes"
148+
>
149+
<UpvoteIcon className="size-4" />
150+
{largeNumberFormat(upvotes)}
151+
</span>
152+
)}
149153
<span className="flex items-center gap-1.5">
150154
<DiscussIcon className="size-4" />
151155
{largeNumberFormat(comments)}

packages/shared/src/components/post/PostContent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export function PostContentRaw({
198198
createdAt={post.createdAt}
199199
readTime={post.readTime}
200200
isVideoType={isVideoType}
201+
showBelowThresholdLabel={false}
201202
className={metadataClassName}
202203
domain={
203204
!isVideoType &&
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import type { Post } from '../../graphql/posts';
4+
import { UserVote } from '../../graphql/posts';
5+
import { PostUpvotesCommentsCount } from './PostUpvotesCommentsCount';
6+
7+
const mockUseConditionalFeature = jest.fn();
8+
const mockUseAuthContext = jest.fn();
9+
const mockOpenModal = jest.fn();
10+
11+
jest.mock('../../hooks/useConditionalFeature', () => ({
12+
useConditionalFeature: () => mockUseConditionalFeature(),
13+
}));
14+
15+
jest.mock('../../contexts/AuthContext', () => ({
16+
useAuthContext: () => mockUseAuthContext(),
17+
}));
18+
19+
jest.mock('../../hooks/useLazyModal', () => ({
20+
useLazyModal: () => ({ openModal: mockOpenModal }),
21+
}));
22+
23+
jest.mock('../../hooks/useCoresFeature', () => ({
24+
useHasAccessToCores: () => false,
25+
}));
26+
27+
jest.mock('../../lib/user', () => ({
28+
canViewPostAnalytics: () => false,
29+
}));
30+
31+
describe('PostUpvotesCommentsCount', () => {
32+
beforeEach(() => {
33+
jest.clearAllMocks();
34+
mockUseAuthContext.mockReturnValue({ user: { id: 'user-id' } });
35+
mockUseConditionalFeature.mockReturnValue({
36+
value: {
37+
threshold: 3,
38+
belowThresholdLabel: 'New',
39+
newWindowHours: 24,
40+
},
41+
});
42+
});
43+
44+
it('does not render below-threshold label on post page', () => {
45+
const post = {
46+
id: 'post-id',
47+
createdAt: new Date().toISOString(),
48+
numUpvotes: 1,
49+
numComments: 0,
50+
numAwards: 0,
51+
numReposts: 0,
52+
userState: {
53+
vote: UserVote.None,
54+
},
55+
} as Post;
56+
57+
render(<PostUpvotesCommentsCount post={post} />);
58+
59+
expect(screen.queryByText('New')).not.toBeInTheDocument();
60+
});
61+
});

packages/shared/src/components/post/PostUpvotesCommentsCount.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,14 @@ export function PostUpvotesCommentsCount({
4545
const reposts = post.numReposts || 0;
4646
const hasAccessToCores = useHasAccessToCores();
4747
const userHasUpvoted = post.userState?.vote === UserVote.Up;
48-
const { showCount: showUpvotes, belowThresholdLabel: upvoteLabel } =
49-
getUpvoteCountDisplay(
50-
upvotes,
51-
upvoteThresholdConfig.threshold,
52-
upvoteThresholdConfig.belowThresholdLabel,
53-
userHasUpvoted,
54-
post.createdAt,
55-
upvoteThresholdConfig.newWindowHours,
56-
);
48+
const { showCount: showUpvotes } = getUpvoteCountDisplay(
49+
upvotes,
50+
upvoteThresholdConfig.threshold,
51+
upvoteThresholdConfig.belowThresholdLabel,
52+
userHasUpvoted,
53+
post.createdAt,
54+
upvoteThresholdConfig.newWindowHours,
55+
);
5756
const onRepostsClick = () =>
5857
openModal({
5958
type: LazyModal.RepostsPopup,
@@ -86,7 +85,6 @@ export function PostUpvotesCommentsCount({
8685
{largeNumberFormat(upvotes)} Upvote{upvotes > 1 ? 's' : ''}
8786
</ClickableText>
8887
)}
89-
{!showUpvotes && !!upvoteLabel && <span>{upvoteLabel}</span>}
9088
{comments > 0 && (
9189
<span>
9290
{largeNumberFormat(comments)}

packages/shared/src/components/post/SocialTwitterPostContent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ function SocialTwitterPostContentRaw({
161161
<PostMetadata
162162
createdAt={post.createdAt}
163163
readTime={post.readTime}
164+
showBelowThresholdLabel={false}
164165
className={classNames('mt-4 !typo-callout', 'mb-4')}
165166
>
166167
{!!post.createdAt && <Separator className="mx-0" />}

0 commit comments

Comments
 (0)