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
6 changes: 0 additions & 6 deletions packages/shared/src/components/FeedItemComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,6 @@ function FeedItemComponent({
onCommentClick,
onReadArticleClick,
virtualizedNumCards,
disableAdRefresh,
}: FeedItemComponentProps): ReactElement | null {
const { logEvent } = useLogContext();
const queryClient = useQueryClient();
Expand Down Expand Up @@ -516,11 +515,6 @@ function FeedItemComponent({
index={item.index}
feedIndex={index}
onLinkClick={(ad: Ad) => onAdAction(AdActions.Click, ad)}
onRefresh={
disableAdRefresh
? undefined
: (ad: Ad) => onAdAction(AdActions.Refresh, ad)
}
/>
);
}
Expand Down
97 changes: 77 additions & 20 deletions packages/shared/src/components/cards/ad/AdCard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,35 @@ import React from 'react';
import type { RenderResult } from '@testing-library/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import { QueryClient } from '@tanstack/react-query';
import { GrowthBook } from '@growthbook/growthbook-react';
import ad from '../../../../__tests__/fixture/ad';
import { AdGrid } from './AdGrid';
import { AdList } from './AdList';
import { SignalAdList } from './SignalAdList';
import type { AdCardProps } from './common/common';
import { TestBootProvider } from '../../../../__tests__/helpers/boot';
import { ActiveFeedContext } from '../../../contexts';
import { businessWebsiteUrl } from '../../../lib/constants';

const defaultProps: AdCardProps = {
ad,
index: 0,
feedIndex: 0,
onLinkClick: jest.fn(),
};

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

const renderComponent = (props: Partial<AdCardProps> = {}): RenderResult => {
const client = new QueryClient();
return render(
<TestBootProvider client={client}>
<ActiveFeedContext.Provider value={{ queryKey: 'test' }}>
<AdGrid {...defaultProps} {...props} />
</ActiveFeedContext.Provider>
</TestBootProvider>,
);
};

const renderListComponent = (
props: Partial<AdCardProps> = {},
gb = new GrowthBook(),
): RenderResult => {
const client = new QueryClient();
return render(
<TestBootProvider client={client}>
<ActiveFeedContext.Provider value={{ queryKey: 'test' }}>
<TestBootProvider client={client} gb={gb}>
<ActiveFeedContext.Provider value={{ items: [], queryKey: ['test'] }}>
<AdList {...defaultProps} {...props} />
</ActiveFeedContext.Provider>
</TestBootProvider>,
Expand All @@ -45,30 +39,58 @@ const renderListComponent = (

const renderSignalListComponent = (
props: Partial<AdCardProps> = {},
gb = new GrowthBook(),
): RenderResult => {
const client = new QueryClient();
return render(
<TestBootProvider client={client}>
<ActiveFeedContext.Provider value={{ queryKey: 'test' }}>
<TestBootProvider client={client} gb={gb}>
<ActiveFeedContext.Provider value={{ items: [], queryKey: ['test'] }}>
<SignalAdList {...defaultProps} {...props} />
</ActiveFeedContext.Provider>
</TestBootProvider>,
);
};

const getGrowthBook = (isAdReferralCtaEnabled = false): GrowthBook => {
const gb = new GrowthBook();

gb.setFeatures({
ad_referral_cta: {
defaultValue: isAdReferralCtaEnabled,
},
});

return gb;
};

const getNormalizedText = (element?: Element | null): string =>
element?.textContent?.replace(/\u200B/g, '').trim() ?? '';

const renderGridComponent = (
props: Partial<AdCardProps> = {},
gb = new GrowthBook(),
): RenderResult => {
const client = new QueryClient();

return render(
<TestBootProvider client={client} gb={gb}>
<ActiveFeedContext.Provider value={{ items: [], queryKey: ['test'] }}>
<AdGrid {...defaultProps} {...props} />
</ActiveFeedContext.Provider>
</TestBootProvider>,
);
};

it('should call on click on component left click', async () => {
renderComponent();
renderGridComponent();
const el = await screen.findByTestId('adItem');
const links = await within(el).findAllByRole('link');
links[0].click();
await waitFor(() => expect(defaultProps.onLinkClick).toBeCalledWith(ad));
});

it('should call on click on component middle mouse up', async () => {
renderComponent();
renderGridComponent();
const el = await screen.findByTestId('adItem');
const links = await within(el).findAllByRole('link');
links[0].dispatchEvent(
Expand All @@ -78,29 +100,46 @@ it('should call on click on component middle mouse up', async () => {
});

it('should show a single image by default', async () => {
renderComponent();
renderGridComponent();
const img = await screen.findByAltText('Ad image');
const background = screen.queryByAltText('Ad image background');
expect(img).toBeInTheDocument();
expect(background).not.toBeInTheDocument();
});

it('should show blurred image for carbon', async () => {
renderComponent({ ad: { ...ad, source: 'Carbon' } });
renderGridComponent({ ad: { ...ad, source: 'Carbon' } });
const img = await screen.findByAltText('Ad image');
const background = screen.queryByAltText('Ad image background');
expect(img).toHaveClass('absolute');
expect(background).toBeInTheDocument();
});

it('should show pixel images', async () => {
renderComponent({
renderGridComponent({
ad: { ...ad, pixel: ['https://daily.dev/pixel'] },
});
const el = await screen.findByTestId('pixel');
expect(el).toHaveAttribute('src', 'https://daily.dev/pixel');
});

it('should not render advertise link by default', () => {
renderGridComponent();

expect(
screen.queryByRole('link', { name: 'Advertise here' }),
).not.toBeInTheDocument();
});

it('should render advertise link on grid ad when feature is enabled', () => {
renderGridComponent({}, getGrowthBook(true));

expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute(
'href',
businessWebsiteUrl,
);
});

it('should render promoted attribution outside of list title clamp', async () => {
const promotedMatcher = (_: string, element?: Element | null): boolean => {
const text = getNormalizedText(element);
Expand All @@ -114,6 +153,15 @@ it('should render promoted attribution outside of list title clamp', async () =>
expect(await screen.findByText(promotedMatcher)).toBeInTheDocument();
});

it('should render advertise link on list ad when feature is enabled', () => {
renderListComponent({}, getGrowthBook(true));

expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute(
'href',
businessWebsiteUrl,
);
});

it('should render company logo and company name in signal ad header', async () => {
const companyLogo = 'https://daily.dev/company-logo.png';
const companyName = 'daily.dev';
Expand All @@ -126,3 +174,12 @@ it('should render company logo and company name in signal ad header', async () =
expect(logo).toHaveAttribute('src', companyLogo);
expect(screen.getByText(companyName)).toBeInTheDocument();
});

it('should render advertise link on signal list ad when feature is enabled', () => {
renderSignalListComponent({}, getGrowthBook(true));

expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute(
'href',
businessWebsiteUrl,
);
});
84 changes: 46 additions & 38 deletions packages/shared/src/components/cards/ad/AdGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactElement } from 'react';
import type { ForwardedRef, ReactElement } from 'react';
import React, { forwardRef, useCallback } from 'react';

import {
Expand All @@ -18,31 +18,39 @@ import { RemoveAd } from './common/RemoveAd';
import { usePlusSubscription } from '../../../hooks/usePlusSubscription';
import type { InViewRef } from '../../../hooks/feed/useAutoRotatingAds';
import { useAutoRotatingAds } from '../../../hooks/feed/useAutoRotatingAds';
import { AdRefresh } from './common/AdRefresh';
import { Button } from '../../buttons/Button';
import { ButtonSize, ButtonVariant } from '../../buttons/common';
import { AdFavicon } from './common/AdFavicon';
import PostTags from '../common/PostTags';
import { useFeature } from '../../GrowthBookProvider';
import { adImprovementsV3Feature } from '../../../lib/featureManagement';
import { TargetId } from '../../../lib/log';
import { AdvertiseLink } from './common/AdvertiseLink';

export const AdGrid = forwardRef(function AdGrid(
{ ad, onLinkClick, onRefresh, domProps, index, feedIndex }: AdCardProps,
inViewRef: InViewRef,
export const AdGrid = forwardRef<HTMLElement, AdCardProps>(function AdGrid(
{ ad, onLinkClick, domProps, index, feedIndex }: AdCardProps,
forwardedRef: ForwardedRef<HTMLElement>,
): ReactElement {
const { isPlus } = usePlusSubscription();
const adImprovementsV3 = useFeature(adImprovementsV3Feature);
const { ref, refetch, isRefetching } = useAutoRotatingAds(
ad,
index,
feedIndex,
inViewRef,
);
const matchingTags = ad.matchingTags ?? [];
const inViewRef = useCallback<InViewRef>(
(node) => {
const nextNode = node as HTMLElement | null;

if (typeof forwardedRef === 'function') {
forwardedRef(nextNode);
return;
}

const onRefreshClick = useCallback(async () => {
onRefresh?.(ad);
await refetch();
}, [ad, onRefresh, refetch]);
if (forwardedRef) {
const forwardedRefObject = forwardedRef;
forwardedRefObject.current = nextNode;
}
},
[forwardedRef],
);
const { ref } = useAutoRotatingAds(ad, index, feedIndex, inViewRef);

return (
<Card {...domProps} data-testid="adItem" ref={ref}>
Expand All @@ -51,9 +59,9 @@ export const AdGrid = forwardRef(function AdGrid(
<CardTextContainer className="flex-1">
<CardTitle className="typo-title3">{ad.description}</CardTitle>
<CardSpace />
{adImprovementsV3 && ad?.matchingTags?.length > 0 ? (
{adImprovementsV3 && matchingTags.length > 0 ? (
<PostTags
post={{ tags: ad.matchingTags.slice(0, 6) }}
post={{ tags: matchingTags.slice(0, 6) }}
className="!items-end"
/>
) : null}
Expand All @@ -62,33 +70,33 @@ export const AdGrid = forwardRef(function AdGrid(
<AdImage className="mx-1 mb-0" ad={ad} ImageComponent={CardImage} />
<CardTextContainer className="!mx-1 my-1">
<div className="flex items-center">
{!!ad.callToAction && (
<Button
tag="a"
href={ad.link}
target="_blank"
rel="noopener"
variant={ButtonVariant.Primary}
size={ButtonSize.Small}
className="z-1"
{...combinedClicks(() => onLinkClick?.(ad))}
>
{ad.callToAction}
</Button>
)}
<div className="ml-auto flex items-center gap-2">
{!!onRefresh && (
<AdRefresh
variant={ButtonVariant.Tertiary}
<div className="flex items-center gap-2">
{!!ad.callToAction && (
<Button
tag="a"
href={ad.link}
target="_blank"
rel="noopener"
variant={ButtonVariant.Primary}
size={ButtonSize.Small}
onClick={onRefreshClick}
loading={isRefetching}
/>
className="z-1 typo-footnote"
{...combinedClicks(() => onLinkClick?.(ad))}
>
{ad.callToAction}
</Button>
)}
<AdvertiseLink
targetId={TargetId.AdCard}
buttonStyle
size={ButtonSize.Small}
/>
</div>
<div className="ml-auto flex items-center gap-2">
{!isPlus && (
<RemoveAd
variant={ButtonVariant.Tertiary}
size={ButtonSize.Small}
className="!font-normal typo-footnote"
/>
)}
</div>
Expand Down
Loading
Loading