diff --git a/dotcom-rendering/src/components/Card/Card.tsx b/dotcom-rendering/src/components/Card/Card.tsx index c85c10761d2..0284c8c0356 100644 --- a/dotcom-rendering/src/components/Card/Card.tsx +++ b/dotcom-rendering/src/components/Card/Card.tsx @@ -164,6 +164,7 @@ export type Props = { headlinePosition?: 'inner' | 'outer'; starRatingSize?: RatingSizeType; contentSpacing?: 'small' | 'large'; + headlineTestUuid?: string; }; const waveformWrapper = ( @@ -410,6 +411,7 @@ export const Card = ({ starRatingSize = 'small', articleMedia, contentSpacing, + headlineTestUuid, }: Props) => { const ab = useAB(); const isInLoopClickTestControl = Boolean( @@ -830,6 +832,7 @@ export const Card = ({ isExternalLink={isExternalLink} isLoopAndInLoopClickTest={isLoopAndInLoopClickTest} shouldRaiseZIndexForAbTest={false} // The z-index is raised in a new CardLink in the SelfHostedVideo island. + headlineTestUuid={headlineTestUuid} /> {headlinePosition === 'outer' && (
{ return ( ); @@ -64,11 +68,13 @@ const ExternalLink = ({ headlineText, dataLinkName, shouldRaiseZIndexForAbTest, + headlineTestUuid, }: { linkTo: string; headlineText: string; dataLinkName?: string; shouldRaiseZIndexForAbTest: boolean; + headlineTestUuid?: string; }) => { return ( { /** * If we are in the loop click through test, we add a unique string to the data link name @@ -109,6 +117,7 @@ export const CardLink = ({ headlineText={headlineText} dataLinkName={clickThroughLinkName} shouldRaiseZIndexForAbTest={shouldRaiseZIndexForAbTest} + headlineTestUuid={headlineTestUuid} /> )} {!isExternalLink && ( @@ -117,6 +126,7 @@ export const CardLink = ({ headlineText={headlineText} dataLinkName={clickThroughLinkName} shouldRaiseZIndexForAbTest={shouldRaiseZIndexForAbTest} + headlineTestUuid={headlineTestUuid} /> )} diff --git a/dotcom-rendering/src/components/FeatureCard.tsx b/dotcom-rendering/src/components/FeatureCard.tsx index f2c069c24fa..f5339105d20 100644 --- a/dotcom-rendering/src/components/FeatureCard.tsx +++ b/dotcom-rendering/src/components/FeatureCard.tsx @@ -407,6 +407,7 @@ export type Props = { */ isImmersive?: boolean; starRatingSize: RatingSizeType; + headlineTestUuid?: string; }; export const FeatureCard = ({ @@ -445,6 +446,7 @@ export const FeatureCard = ({ isImmersive = false, starRatingSize, articleMedia, + headlineTestUuid, }: Props) => { const ab = useAB(); const isInLoopClickTestControl = Boolean( @@ -546,6 +548,7 @@ export const FeatureCard = ({ shouldRaiseZIndexForAbTest={ isLoopAndInLoopClickTestVariant } + headlineTestUuid={headlineTestUuid} /> )}
@@ -769,6 +772,9 @@ export const FeatureCard = ({ shouldRaiseZIndexForAbTest={ isLoopAndInLoopClickTestVariant } + headlineTestUuid={ + headlineTestUuid + } /> )} diff --git a/dotcom-rendering/src/components/FlexibleGeneral.tsx b/dotcom-rendering/src/components/FlexibleGeneral.tsx index 82e2bee2faa..cde38b02a64 100644 --- a/dotcom-rendering/src/components/FlexibleGeneral.tsx +++ b/dotcom-rendering/src/components/FlexibleGeneral.tsx @@ -141,6 +141,7 @@ const ImmersiveCardLayout = ({ isImmersive={true} starRatingSize="medium" articleMedia={card.articleMedia} + headlineTestUuid={card.headlineTestUuid} /> diff --git a/dotcom-rendering/src/components/FrontCard.tsx b/dotcom-rendering/src/components/FrontCard.tsx index ea9bad0d677..930629a12e6 100644 --- a/dotcom-rendering/src/components/FrontCard.tsx +++ b/dotcom-rendering/src/components/FrontCard.tsx @@ -59,6 +59,7 @@ export const FrontCard = (props: Props) => { starRating: trail.starRating, uniqueId: trail.uniqueId, webPublicationDate: trail.webPublicationDate, + headlineTestUuid: trail.headlineTestUuid, }; return Card({ ...defaultProps, ...cardProps }); diff --git a/dotcom-rendering/src/components/Masthead/HighlightsCard.tsx b/dotcom-rendering/src/components/Masthead/HighlightsCard.tsx index 57279975859..786f2a43e41 100644 --- a/dotcom-rendering/src/components/Masthead/HighlightsCard.tsx +++ b/dotcom-rendering/src/components/Masthead/HighlightsCard.tsx @@ -37,6 +37,7 @@ export type HighlightsCardProps = { byline?: string; isExternalLink: boolean; starRating?: Rating; + headlineTestUuid?: string; }; const container = css` @@ -135,6 +136,7 @@ export const HighlightsCard = ({ isExternalLink, starRating, articleMedia, + headlineTestUuid, }: HighlightsCardProps) => { const isMediaCard = isMedia(format); @@ -154,6 +156,7 @@ export const HighlightsCard = ({ isExternalLink={isExternalLink} isLoopAndInLoopClickTest={false} shouldRaiseZIndexForAbTest={false} + headlineTestUuid={headlineTestUuid} />
diff --git a/dotcom-rendering/src/components/ScrollableFeature.island.tsx b/dotcom-rendering/src/components/ScrollableFeature.island.tsx index 4a23a22de8f..9df79e8452c 100644 --- a/dotcom-rendering/src/components/ScrollableFeature.island.tsx +++ b/dotcom-rendering/src/components/ScrollableFeature.island.tsx @@ -91,6 +91,7 @@ export const ScrollableFeature = ({ showQuotes={card.showQuotedHeadline} starRatingSize={'small'} articleMedia={card.articleMedia} + headlineTestUuid={card.headlineTestUuid} /> ))} diff --git a/dotcom-rendering/src/components/ScrollableHighlights.island.tsx b/dotcom-rendering/src/components/ScrollableHighlights.island.tsx index e98a44673ed..65f33f9a4c9 100644 --- a/dotcom-rendering/src/components/ScrollableHighlights.island.tsx +++ b/dotcom-rendering/src/components/ScrollableHighlights.island.tsx @@ -341,6 +341,7 @@ export const ScrollableHighlights = ({ trails, frontId }: Props) => { mainMedia={trail.mainMedia} starRating={trail.starRating} articleMedia={trail.articleMedia} + headlineTestUuid={trail.headlineTestUuid} /> )} diff --git a/dotcom-rendering/src/components/StaticFeatureTwo.tsx b/dotcom-rendering/src/components/StaticFeatureTwo.tsx index 05d73a0c092..dd8e149d611 100644 --- a/dotcom-rendering/src/components/StaticFeatureTwo.tsx +++ b/dotcom-rendering/src/components/StaticFeatureTwo.tsx @@ -80,6 +80,7 @@ export const StaticFeatureTwo = ({ showQuotes={card.showQuotedHeadline} starRatingSize={'medium'} articleMedia={card.articleMedia} + headlineTestUuid={card.headlineTestUuid} /> ); diff --git a/dotcom-rendering/src/components/SupportingContent.tsx b/dotcom-rendering/src/components/SupportingContent.tsx index 5c5a6f75e4e..db040c2f679 100644 --- a/dotcom-rendering/src/components/SupportingContent.tsx +++ b/dotcom-rendering/src/components/SupportingContent.tsx @@ -186,6 +186,7 @@ export const SupportingContent = ({ : verticalSublinkStyles, ]} data-link-name={`sublinks | ${index + 1}`} + data-fronts-test-uuid={subLink.headlineTestUuid} > { }); }); - describe('decideHeadline', () => { + describe('getEditorialTestData', () => { const cardWithNoEditorialTest = { properties: { isBreaking: false, @@ -667,26 +667,31 @@ describe('Enhance Cards', () => { supportingContent: [cardWithExpiredEditorialTest], }; - it('returns the default headline if no editorial test exists on the card, page is not in allowed fronts list, and user is not in a test bucket', () => { + it('returns undefined if no editorial test exists on the card, page is not in allowed fronts list, and user is not in a test bucket', () => { expect( - decideHeadline( + getEditorialTestData( cardWithNoEditorialTest, {}, true, 'invalid-test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns the default headline if editorial test exists and page is in allowed fronts list, but user is not in a test bucket', () => { + it('returns undefined if editorial test exists and page is in allowed fronts list, but user is not in a test bucket', () => { expect( - decideHeadline(cardWithEditorialTest, {}, true, 'test-front'), - ).toEqual('Headline'); + getEditorialTestData( + cardWithEditorialTest, + {}, + true, + 'test-front', + ), + ).toEqual(undefined); }); - it('returns the default headline if user is in a test bucket and page is in allowed fronts list, but editorial test does not exist', () => { + it('returns undefined if user is in a test bucket and page is in allowed fronts list, but editorial test does not exist', () => { expect( - decideHeadline( + getEditorialTestData( cardWithNoEditorialTest, { 'fronts-and-curation-editorial-test': 'a', @@ -694,12 +699,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns the default headline if editorial test exists and user is in a test bucket, but page is not in allowed fronts list', () => { + it('returns undefined if editorial test exists and user is in a test bucket, but page is not in allowed fronts list', () => { expect( - decideHeadline( + getEditorialTestData( cardWithEditorialTest, { 'fronts-and-curation-editorial-test': 'a', @@ -707,12 +712,12 @@ describe('Enhance Cards', () => { true, 'invalid-test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns the default headline if editorial test exists, page is in allowed fronts list, user is in a variant bucket, but the feature switch is turned off ', () => { + it('returns undefined if editorial test exists, page is in allowed fronts list, user is in a variant bucket, but the feature switch is turned off ', () => { expect( - decideHeadline( + getEditorialTestData( cardWithEditorialTest, { 'fronts-and-curation-editorial-test': 'a', @@ -720,12 +725,12 @@ describe('Enhance Cards', () => { false, 'test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns headline A if editorial test exists, page is in allowed fronts list, and user is in bucket A', () => { + it('returns headline A & test uuid if editorial test exists, page is in allowed fronts list, and user is in bucket A', () => { expect( - decideHeadline( + getEditorialTestData( cardWithEditorialTest, { 'fronts-and-curation-editorial-test': 'a', @@ -733,12 +738,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline A'); + ).toEqual({ headline: 'Headline A', uuid: 'uuid' }); }); - it('returns headline B if editorial test exists, page is in allowed fronts list, and user is in bucket B', () => { + it('returns headline B & test uuid if editorial test exists, page is in allowed fronts list, and user is in bucket B', () => { expect( - decideHeadline( + getEditorialTestData( cardWithEditorialTest, { 'fronts-and-curation-editorial-test': 'b', @@ -746,12 +751,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline B'); + ).toEqual({ headline: 'Headline B', uuid: 'uuid' }); }); - it('returns the default headline if the bucket name does not match a variant meta id', () => { + it('returns undefined if the bucket name does not match a variant meta id', () => { expect( - decideHeadline( + getEditorialTestData( cardWithEditorialTest, { 'fronts-and-curation-editorial-test': 'c', @@ -759,12 +764,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns the default headline if the variant headline is undefined', () => { + it('returns undefined if the variant headline is undefined', () => { expect( - decideHeadline( + getEditorialTestData( cardWithEditorialTestWithUndefinedVariantMeta, { 'fronts-and-curation-editorial-test': 'a', @@ -772,12 +777,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns the default headline if an editorial test has expired', () => { + it('returns undefined if an editorial test has expired', () => { expect( - decideHeadline( + getEditorialTestData( cardWithExpiredEditorialTest, { 'fronts-and-curation-editorial-test': 'a', @@ -785,12 +790,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns the default headline if an editorial test has been manually ended', () => { + it('returns undefined if an editorial test has been manually ended', () => { expect( - decideHeadline( + getEditorialTestData( cardWithManuallyEndedEditorialTest, { 'fronts-and-curation-editorial-test': 'a', @@ -798,12 +803,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); - it('returns the variant headline if an editorial test is present on a sublink', () => { + it('returns the variant headline & test uuid if an editorial test is present on a sublink', () => { expect( - decideHeadline( + getEditorialTestData( cardWithSublinkWithEditorialTest .supportingContent[0] as FESupportingContent, { @@ -812,12 +817,12 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline A'); + ).toEqual({ headline: 'Headline A', uuid: 'uuid' }); }); - it('returns the default headline for a sublink if an editorial test is expired on a sublink', () => { + it('returns undefined for a sublink if an editorial test is expired on a sublink', () => { expect( - decideHeadline( + getEditorialTestData( cardWithSublinkWithExpiredEditorialTest .supportingContent[0] as FESupportingContent, { @@ -826,7 +831,7 @@ describe('Enhance Cards', () => { true, 'test-front', ), - ).toEqual('Headline'); + ).toEqual(undefined); }); }); }); diff --git a/dotcom-rendering/src/model/enhanceCards.ts b/dotcom-rendering/src/model/enhanceCards.ts index 97b64535893..5dde8bf48f1 100644 --- a/dotcom-rendering/src/model/enhanceCards.ts +++ b/dotcom-rendering/src/model/enhanceCards.ts @@ -62,6 +62,12 @@ const enhanceSupportingContent = ( url: decideUrl(subLink), kickerText: !kickerText && supportingContentIsLive ? 'Live' : kickerText, + headlineTestUuid: findHeadlineTestUuid( + subLink, + serverSideABTests, + isEditorialABTestingEnabled, + pageId, + ), }; }); }; @@ -212,46 +218,99 @@ const findActiveEditorialTest = ( return tests?.find((test) => isActiveEditorialTest(test)); }; +type EditorialTestData = { + headline: string; + uuid: string; +}; + /** - * Decide the headline to be shown for a given card. If there is an active editorial test on a card, - * return the variant headline matching the user test group. Otherwise, return the default headline + * Centralises the logic used by both the decideHeadline and findHeadlineTestUuid functions. Makes sure that + * the the testing switch is enabled, the page is in the test bucket, the test can run on the given front, + * and that a valid variant headline is defined before returning the variant headline and test UUID. */ -export const decideHeadline = ( +export const getEditorialTestData = ( faciaCard: FEFrontCard | FESupportingContent, serverSideABTests: Record, isEditorialABTestingEnabled: boolean, pageId?: string, -): string => { - const defaultHeadline = faciaCard.header.headline; - - const testBucket = serverSideABTests['fronts-and-curation-editorial-test']; - +): EditorialTestData | undefined => { const activeEditorialTest = findActiveEditorialTest( faciaCard.properties.tests, ); - if ( - !isEditorialABTestingEnabled || - isUndefined(testBucket) || - !activeEditorialTest - ) { - return defaultHeadline; + // don't return data if there is no active test on the card or editorial testing is switched off + if (!activeEditorialTest || !isEditorialABTestingEnabled) { + return undefined; } const testCanRunOnPage = !isUndefined(pageId) && activeEditorialTest.frontsThisTestCanRunOn.includes(pageId); - if (!testCanRunOnPage) return defaultHeadline; + // don't return data if test cannot run on the current front + if (!testCanRunOnPage) return undefined; + + const testBucket = serverSideABTests['fronts-and-curation-editorial-test']; + + // don't return data if the pageview is not in the editorial test bucket + if (isUndefined(testBucket)) { + return undefined; + } const variantMeta = activeEditorialTest.variantMeta.find( (variant) => variant.id.toLowerCase() === testBucket, ); // make sure the variant headline isn't undefined and that it is of type string - if (typeof variantMeta?.meta.headline !== 'string') return defaultHeadline; + if (typeof variantMeta?.meta.headline !== 'string') return undefined; + + return { + headline: variantMeta.meta.headline, + uuid: activeEditorialTest.testUuid, + }; +}; + +/** + * Decide the headline to be shown for a given card. If there is an active editorial test on a card, + * return the variant headline matching the user test group. Otherwise, return the default headline + */ +export const decideHeadline = ( + faciaCard: FEFrontCard | FESupportingContent, + serverSideABTests: Record, + isEditorialABTestingEnabled: boolean, + pageId?: string, +): string => { + const editorialTestData = getEditorialTestData( + faciaCard, + serverSideABTests, + isEditorialABTestingEnabled, + pageId, + ); + + if (isUndefined(editorialTestData)) return faciaCard.header.headline; + + return editorialTestData.headline; +}; + +/** + * Find the UUID of a headline test if one is running on the card + */ +export const findHeadlineTestUuid = ( + faciaCard: FEFrontCard | FESupportingContent, + serverSideABTests: Record, + isEditorialABTestingEnabled: boolean, + pageId?: string, +): string | undefined => { + const editorialTestData = getEditorialTestData( + faciaCard, + serverSideABTests, + isEditorialABTestingEnabled, + pageId, + ); + + if (isUndefined(editorialTestData)) return undefined; - return variantMeta.meta.headline; + return editorialTestData.uuid; }; /** @@ -596,5 +655,11 @@ export const enhanceCards = ( ?.allImages[0]?.fields.altText ?? '', }, }), + headlineTestUuid: findHeadlineTestUuid( + faciaCard, + serverSideABTests, + isEditorialABTestingEnabled, + pageId, + ), }; }); diff --git a/dotcom-rendering/src/types/front.ts b/dotcom-rendering/src/types/front.ts index d69e654cd0c..79cc7c114bb 100644 --- a/dotcom-rendering/src/types/front.ts +++ b/dotcom-rendering/src/types/front.ts @@ -106,6 +106,7 @@ export type DCRFrontCard = { uniqueId?: string; serverTime?: number; articleMedia?: ArticleMedia; + headlineTestUuid?: string; }; export type DCRSlideshowImage = { @@ -153,6 +154,7 @@ export type DCRSupportingContent = { url?: string; kickerText?: string; format: ArticleFormat; + headlineTestUuid?: string; }; export type TreatType = {