Skip to content
243 changes: 242 additions & 1 deletion dotcom-rendering/src/model/enhanceCards.test.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for being so thorough with this test suite ✨

Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { FEMediaAsset, FEMediaAtom } from '../frontend/feFront';
import type {
FEFrontCardStyle,
FEMediaAsset,
FEMediaAtom,
} from '../frontend/feFront';
import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat';
import type { EditorialTest, VariantMeta } from '../types/front';
import type { MainMedia } from '../types/mainMedia';
import {
decideArticleMedia,
decideHeadline,
decideReplacementMedia,
getActiveMediaAtom,
getMediaMetadata,
Expand Down Expand Up @@ -527,4 +533,239 @@ describe('Enhance Cards', () => {
});
});
});

describe('decideHeadline', () => {
const cardWithNoEditorialTest = {
properties: {
isBreaking: false,
showKickerTag: false,
showByline: false,
isLiveBlog: false,
isCrossword: false,
webTitle: '',
editionBrandings: [],
tests: [],
},
header: {
isVideo: false,
isComment: false,
isGallery: false,
isAudio: false,
headline: 'Headline',
url: '',
hasMainVideoElement: false,
},
card: {
id: '',
cardStyle: {
type: 'DefaultCardstyle' as FEFrontCardStyle,
},
shortUrl: '',
group: '',
isLive: false,
},
discussion: {
isCommentable: false,
isClosedForComments: false,
},
display: {
isBoosted: false,
showBoostedHeadline: false,
showQuotedHeadline: false,
imageHide: false,
showLivePlayable: false,
},
type: '',
};

const oneHourInMilliseconds = 60 * 60 * 1000;

const cardWithEditorialTest = {
...cardWithNoEditorialTest,
properties: {
...cardWithNoEditorialTest.properties,
tests: [
{
testUuid: 'uuid',
variantMeta: [
{
id: 'A',
meta: {
headline: 'Headline A',
},
},
{
id: 'B',
meta: {
headline: 'Headline B',
},
},
] as VariantMeta[],
startDate: Date.now() - oneHourInMilliseconds,
expiryDate: Date.now() + oneHourInMilliseconds,
frontsThisTestCanRunOn: ['test-front'],
hasManuallyEndedOnThisTrail: false,
},
],
},
};

const cardWithEditorialTestWithUndefinedVariantMeta = {
...cardWithEditorialTest,
properties: {
...cardWithEditorialTest.properties,
tests: [
{
...cardWithEditorialTest.properties.tests[0],
variantMeta: [
{
id: 'A',
meta: {
headline: undefined,
},
} as VariantMeta,
],
} as EditorialTest,
],
},
};

const cardWithExpiredEditorialTest = {
...cardWithEditorialTest,
properties: {
...cardWithEditorialTest.properties,
tests: [
{
...cardWithEditorialTest.properties.tests[0],
expiryDate: Date.now() - oneHourInMilliseconds,
} as EditorialTest,
],
},
};

const cardWithManuallyEndedEditorialTest = {
...cardWithEditorialTest,
properties: {
...cardWithEditorialTest.properties,
tests: [
{
...cardWithEditorialTest.properties.tests[0],
hasManuallyEndedOnThisTrail: true,
} as EditorialTest,
],
},
};

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', () => {
expect(
decideHeadline(
cardWithNoEditorialTest,
{},
'invalid-test-front',
),
).toEqual('Headline');
});

it('returns the default headline if editorial test exists and page is in allowed fronts list, but user is not in a test bucket', () => {
expect(
decideHeadline(cardWithEditorialTest, {}, 'test-front'),
).toEqual('Headline');
});

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', () => {
expect(
decideHeadline(
cardWithNoEditorialTest,
{
'fronts-and-curation-editorial-headline-test': 'a',
},
'test-front',
),
).toEqual('Headline');
});

it('returns the default headline if editorial test exists and user is in a test bucket, but page is not in allowed fronts list', () => {
expect(
decideHeadline(
cardWithEditorialTest,
{
'fronts-and-curation-editorial-headline-test': 'a',
},
'invalid-test-front',
),
).toEqual('Headline');
});

it('returns headline A if editorial test exists, page is in allowed fronts list, and user is in bucket A', () => {
expect(
decideHeadline(
cardWithEditorialTest,
{
'fronts-and-curation-editorial-headline-test': 'a',
},
'test-front',
),
).toEqual('Headline A');
});

it('returns headline B if editorial test exists, page is in allowed fronts list, and user is in bucket B', () => {
expect(
decideHeadline(
cardWithEditorialTest,
{
'fronts-and-curation-editorial-headline-test': 'b',
},
'test-front',
),
).toEqual('Headline B');
});

it('returns the default headline if the bucket name does not match a variant meta id', () => {
expect(
decideHeadline(
cardWithEditorialTest,
{
'fronts-and-curation-editorial-headline-test': 'c',
},
'test-front',
),
).toEqual('Headline');
});

it('returns the default headline if the variant headline is undefined', () => {
expect(
decideHeadline(
cardWithEditorialTestWithUndefinedVariantMeta,
{
'fronts-and-curation-editorial-headline-test': 'a',
},
'test-front',
),
).toEqual('Headline');
});

it('returns the default headline if an editorial test has expired', () => {
expect(
decideHeadline(
cardWithExpiredEditorialTest,
{
'fronts-and-curation-editorial-headline-test': 'a',
},
'test-front',
),
).toEqual('Headline');
});

it('returns the default headline if an editorial test has been manually ended', () => {
expect(
decideHeadline(
cardWithManuallyEndedEditorialTest,
{
'fronts-and-curation-editorial-headline-test': 'a',
},
'test-front',
),
).toEqual('Headline');
});
});
});
62 changes: 61 additions & 1 deletion dotcom-rendering/src/model/enhanceCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
DCRFrontCard,
DCRSlideshowImage,
DCRSupportingContent,
EditorialTest,
} from '../types/front';
import type { ArticleMedia, MainMedia } from '../types/mainMedia';
import type { PodcastSeriesImage, TagType } from '../types/tag';
Expand Down Expand Up @@ -183,6 +184,63 @@ const decideVideoAtomImage = (
return undefined;
};

/**
* Checks if an editorial test is active by making sure it has not been manually ended,
* that it has a valid expiry date, and that the expiry date is in the future
*/
const isActiveEditorialTest = (test: EditorialTest) =>
!test.hasManuallyEndedOnThisTrail &&
!!test.expiryDate &&
test.expiryDate > Date.now();

/**
* Looks through a list of editorial tests to see if there is an active test. If no active
* test is found, return undefined
*/
const findActiveEditorialTest = (
tests: EditorialTest[] | undefined,
): EditorialTest | undefined => {
return tests?.find((test) => isActiveEditorialTest(test));
};

/**
* 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,
serverSideABTests: Record<string, string>,
pageId?: string,
): string => {
const defaultHeadline = faciaCard.header.headline;

const testBucket =
serverSideABTests['fronts-and-curation-editorial-headline-test'];

const activeEditorialTest = findActiveEditorialTest(
faciaCard.properties.tests,
);

if (isUndefined(testBucket) || !activeEditorialTest) {
return defaultHeadline;
}

const testCanRunOnPage =
!isUndefined(pageId) &&
activeEditorialTest.frontsThisTestCanRunOn.includes(pageId);

if (!testCanRunOnPage) return defaultHeadline;

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;
Comment on lines +238 to +239

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 thanks for tightening this check


return variantMeta.meta.headline;
};

/**
* While the first Media Atom is *not* guaranteed to be the main media,
* it *happens to be* correct in the majority of cases.
Expand Down Expand Up @@ -363,6 +421,7 @@ export const enhanceCards = (
pageId,
discussionApiUrl,
stripBranding = false,
serverSideABTests,
}: {
cardInTagPage: boolean;
/** Used for the data link name to indicate card position in container */
Expand All @@ -372,6 +431,7 @@ export const enhanceCards = (
discussionApiUrl: string;
/** We strip branding from cards if the branding will appear at the collection level instead */
stripBranding?: boolean;
serverSideABTests: Record<string, string>;
},
): DCRFrontCard[] =>
collections.map((faciaCard, index) => {
Expand Down Expand Up @@ -448,7 +508,7 @@ export const enhanceCards = (
format,
dataLinkName,
url: decideUrl(faciaCard),
headline: faciaCard.header.headline,
headline: decideHeadline(faciaCard, serverSideABTests, pageId),
trailText: faciaCard.card.trailText,
starRating: faciaCard.card.starRating,
webPublicationDate: !isUndefined(
Expand Down
8 changes: 8 additions & 0 deletions dotcom-rendering/src/model/enhanceCollections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const enhanceCollections = ({
frontBranding,
onPageDescription,
isOnPaidContentFront,
serverSideABTests,
}: {
collections: FECollection[];
editionId: EditionId;
Expand All @@ -62,6 +63,7 @@ export const enhanceCollections = ({
frontBranding: Branding | undefined;
onPageDescription?: string;
isOnPaidContentFront?: boolean;
serverSideABTests: Record<string, string>;
}): DCRCollectionType[] => {
const indexToShowFrontBranding =
findCollectionSuitableForFrontBranding(collections);
Expand Down Expand Up @@ -128,18 +130,24 @@ export const enhanceCollections = ({
editionId,
discussionApiUrl,
stripBrandingFromCards,
serverSideABTests,
pageId,
),
curated: enhanceCards(collection.curated, {
cardInTagPage: false,
editionId,
pageId,
discussionApiUrl,
stripBranding: stripBrandingFromCards,
serverSideABTests,
}),
backfill: enhanceCards(collection.backfill, {
cardInTagPage: false,
editionId,
pageId,
discussionApiUrl,
stripBranding: stripBrandingFromCards,
serverSideABTests,
}),
treats: enhanceTreats(
collection.treats,
Expand Down
Loading
Loading