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
4 changes: 2 additions & 2 deletions packages/shared/src/components/Feed.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2451,7 +2451,7 @@ describe('Feed ad cadence with highlight cards', () => {
renderWithHighlightLayout({
posts: [buildPost('p0')],
highlightEnabled: false,
feedName: OtherFeedPage.Squad,
feedName: OtherFeedPage.Squads,
});

await waitFor(() => {
Expand Down Expand Up @@ -2534,7 +2534,7 @@ describe('Feed ad cadence with highlight cards', () => {
buildPost('p3'),
],
highlightEnabled: false,
feedName: OtherFeedPage.Squad,
feedName: OtherFeedPage.Squads,
});

expect(
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export default function Feed<T>({
const { openNewTab, loadedSettings } = useContext(SettingsContext);
const { isListMode, shouldUseListFeedLayout } = useFeedLayout();
const numCards = currentSettings.numCards.eco;
const isSquadFeed = feedName === OtherFeedPage.Squad;
const isSquadFeed = feedName === OtherFeedPage.Squads;
const trackedFeedFinish = useRef(false);
const isMyFeed = feedName === SharedFeedPage.MyFeed;
const showAcquisitionForm =
Expand Down
129 changes: 129 additions & 0 deletions packages/shared/src/components/brand/adFreeSponsorships.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { ReactElement } from 'react';
import React from 'react';
import { QueryClient } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { TestBootProvider } from '../../../__tests__/helpers/boot';
import { defaultQueryClientTestingConfig } from '../../../__tests__/helpers/tanstack-query';
import loggedUser from '../../../__tests__/fixture/loggedUser';
import defaultPost from '../../../__tests__/fixture/post';
import type { Post } from '../../graphql/posts';
import { UserVote } from '../../graphql/posts';
import type { EngagementCreative } from '../../lib/engagementAds';
import { EngagementAdsProvider } from '../../contexts/EngagementAdsContext';
import { ActivePostContextProvider } from '../../contexts/ActivePostContext';
import { PostTagList } from '../post/tags/PostTagList';
import { PostActions } from '../post/PostActions';
import ShowMoreContent from '../cards/common/ShowMoreContent';
import { MentionedToolsWidget } from './MentionedToolsWidget';

jest.mock('../../features/profile/hooks/useUserStack', () => ({
useUserStack: jest.fn(() => ({
stackItems: [],
add: jest.fn(),
remove: jest.fn(),
})),
}));

const creative: EngagementCreative = {
gen_id: 'c1',
promoted_name: 'Copilot',
promoted_body: 'AI pair programming',
promoted_cta: 'Try free',
promoted_url: 'https://example.com',
promoted_logo_img: {
dark: 'https://example.com/logo-dark.png',
light: 'https://example.com/logo-light.png',
},
promoted_icon_img: {
dark: 'https://example.com/icon-dark.png',
light: 'https://example.com/icon-light.png',
},
promoted_gradient_start: { dark: '#000', light: '#fff' },
promoted_gradient_end: { dark: '#111', light: '#eee' },
tools: ['VSCode'],
keywords: ['Copilot'],
tags: ['ai'],
placements: [],
};

const regularPost: Post = {
...defaultPost,
tags: ['ai', 'react'],
userState: { vote: UserVote.None },
};

const adFreePost: Post = {
...regularPost,
source: {
...defaultPost.source,
features: { verified: true, adFree: true, links: false, products: false },
} as Post['source'],
};

const wrap = (post: Post, ui: ReactElement) => (
<TestBootProvider
client={new QueryClient(defaultQueryClientTestingConfig)}
auth={{ user: loggedUser }}
>
<EngagementAdsProvider rawCreatives={[creative]}>
<ActivePostContextProvider post={post}>{ui}</ActivePostContextProvider>
</EngagementAdsProvider>
</TestBootProvider>
);

const cases = [
['shown for a regular source', regularPost, true],
['hidden for an ad-free squad', adFreePost, false],
] as const;

describe('brand sponsorships on a post', () => {
it.each(cases)(
'sponsored tag branding is %s, the plain tags stay',
(_, post, isShown) => {
render(wrap(post, <PostTagList post={post} />));

const list = screen.getByRole('list', { name: 'Post tags' });
expect(list).toHaveTextContent('react');
expect(list).toHaveTextContent('ai');
expect(screen.queryAllByText(/sponsored by Copilot/).length > 0).toBe(
isShown,
);
},
);

it.each(cases)(
'the sponsored keyword highlight in the summary is %s',
(_, post, isShown) => {
render(
wrap(post, <ShowMoreContent content="Try Copilot for AI coding" />),
);

expect(screen.getByTestId('tldr-container')).toHaveTextContent(
'Try Copilot for AI coding',
);
expect(!!screen.queryByText('Copilot', { selector: 'span' })).toBe(
isShown,
);
},
);

it.each(cases)('the sponsored tools widget is %s', (_, post, isShown) => {
render(wrap(post, <MentionedToolsWidget postTags={post.tags ?? []} />));

expect(!!screen.queryByText('Sponsored tools')).toBe(isShown);
});

it.each(cases)('the branded upvote animation is %s', (_, post, isShown) => {
const { rerender, container } = render(
wrap(post, <PostActions post={post} postQueryKey={['post']} />),
);
const upvoted = { ...post, userState: { vote: UserVote.Up } };
rerender(
wrap(upvoted, <PostActions post={upvoted} postQueryKey={['post']} />),
);

// The particle canvas has no role; it mounts only for a branded upvote.
// eslint-disable-next-line testing-library/no-container
expect(!!container.querySelector('canvas')).toBe(isShown);
});
});
43 changes: 10 additions & 33 deletions packages/shared/src/components/cards/squad/SquadGrid.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,13 @@ import { AuthContextProvider } from '../../../contexts/AuthContext';
import loggedUser from '../../../../__tests__/fixture/loggedUser';
import {
generateMembersList,
generateMembersResult,
generateTestAdmin,
generateTestSquad,
} from '../../../../__tests__/fixture/squads';
import { SquadGrid } from './SquadGrid';
import { LazyModalElement } from '../../modals/LazyModalElement';
import type { MockedGraphQLResponse } from '../../../../__tests__/helpers/graphql';
import { mockGraphQL } from '../../../../__tests__/helpers/graphql';
import type { SquadEdgesData } from '../../../graphql/squads';
import {
SQUAD_JOIN_MUTATION,
SQUAD_MEMBERS_QUERY,
} from '../../../graphql/squads';
import { SQUAD_JOIN_MUTATION } from '../../../graphql/squads';
import { waitForNock } from '../../../../__tests__/helpers/utilities';
import { cloudinarySquadsDirectoryCardBannerDefault } from '../../../lib/image';
import { ActionType, COMPLETE_ACTION_MUTATION } from '../../../graphql/actions';
Expand All @@ -34,29 +28,6 @@ const members = generateMembersList();
const admin = generateTestAdmin();
admin.source.members!.edges = members;
admin.source.membersCount = members.length;
const defaultSquad = generateTestSquad();

const createSourceMembersMock = (
result = generateMembersResult(),
variables: Record<string, unknown> = { id: defaultSquad.id, first: 5 },
): MockedGraphQLResponse<SquadEdgesData> => ({
request: { query: SQUAD_MEMBERS_QUERY, variables },
result: { data: result },
});

const openedMembersModal = async () => {
const result = generateMembersResult(members);
mockGraphQL(
createSourceMembersMock(result, { id: defaultSquad.id, role: null }),
);
const membersCount = result.sourceMembers.edges.length;
const trigger = await screen.findByLabelText(
`View ${membersCount} squad members`,
);
trigger.click();
await screen.findByText('Squad members');
return members;
};

beforeEach(async () => {
nock.cleanAll();
Expand Down Expand Up @@ -121,10 +92,16 @@ it('should render the component and member short list when members are provided'
);
});

it('should show the admin on top of the list', async () => {
it('should link the member short list to the members page', async () => {
renderComponent();
const membersList = await openedMembersModal();
expect(membersList[0].node.user.name).toEqual('Eliz Kılıç');
const list = await screen.findByLabelText(
`View ${admin.source.membersCount} squad members`,
);

expect(list).toHaveAttribute(
'href',
expect.stringMatching(/squads\/test\/members$/),
);
});

it('should render the component with a view squad button', async () => {
Expand Down
14 changes: 11 additions & 3 deletions packages/shared/src/components/comments/AdAsComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { AdPixel } from '../cards/ad/common/AdPixel';
import { AdViewability } from '../cards/ad/common/AdViewability';
import type { ViewabilityData } from '../../features/monetization/viewability';
import { viewabilityLogExtra } from '../../features/monetization/viewability';
import { AdActions, AdPlacement } from '../../lib/ads';
import { AdActions, AdPlacement, shouldSkipSourceAds } from '../../lib/ads';
import type { Post } from '../../graphql/posts';
import { usePlusSubscription } from '../../hooks/usePlusSubscription';
import { RemoveAd } from '../cards/ad/common/RemoveAd';
import { AdRefresh } from '../cards/ad/common/AdRefresh';
Expand All @@ -27,11 +28,17 @@ import { AdvertiseLink } from '../cards/ad/common/AdvertiseLink';

interface AdAsCommentProps {
postId: string;
/** The post's source; an ad-free squad gets no ad and no ad request. */
source: Post['source'];
}
export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => {
export const AdAsComment = ({
postId,
source: postSource,
}: AdAsCommentProps): ReactElement => {
const { logEvent } = useLogContext();
const { user } = useAuthContext();
const { isPlus } = usePlusSubscription();
const isAdFree = shouldSkipSourceAds(postSource);

const {
data: ad,
Expand All @@ -41,6 +48,7 @@ export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => {
} = useAdQuery({
placement: AdPlacement.PostComment,
queryKey: generateQueryKey(RequestKey.Ads, user, postId),
enabled: !isAdFree,
staleTime: StaleTime.OneHour,
});

Expand Down Expand Up @@ -93,7 +101,7 @@ export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => {
);
}

if (!ad) {
if (isAdFree || !ad) {
return <></>;
}

Expand Down
Loading
Loading