Skip to content

Commit

Permalink
Merge branch 'latest' into upgrade-express
Browse files Browse the repository at this point in the history
  • Loading branch information
amoore108 authored Oct 21, 2024
2 parents 267b5d0 + e9c6b27 commit 4fcac7c
Show file tree
Hide file tree
Showing 10 changed files with 503 additions and 12 deletions.
86 changes: 86 additions & 0 deletions src/app/components/AmpExperiment/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { render, waitFor } from '../react-testing-library-with-providers';
import AmpExperiment from './index';

const experimentConfig = {
someExperiment: {
variants: {
control: 33,
variant_1: 33,
variant_2: 33,
},
},
};

const multipleExperimentConfig = {
aExperiment: {
variants: {
control: 33,
variant_1: 33,
variant_2: 33,
},
},
bExperiment: {
variants: {
control: 33,
variant_1: 33,
variant_2: 33,
},
},
};

describe('Amp experiment container on Amp pages', () => {
it('should render an amp-experiment with the expected config', async () => {
const { container } = render(
<AmpExperiment experimentConfig={experimentConfig} />,
);
expect(container.querySelector('amp-experiment')).toBeInTheDocument();
expect(container).toMatchInlineSnapshot(`
<div>
<amp-experiment>
<script
type="application/json"
>
{"someExperiment":{"variants":{"control":33,"variant_1":33,"variant_2":33}}}
</script>
</amp-experiment>
</div>
`);
});

it('should render an amp-experiment with the expected config when multiple experiments are running at the same time', async () => {
const { container } = render(
<AmpExperiment experimentConfig={multipleExperimentConfig} />,
);
expect(container.querySelector('amp-experiment')).toBeInTheDocument();
expect(container).toMatchInlineSnapshot(`
<div>
<amp-experiment>
<script
type="application/json"
>
{"aExperiment":{"variants":{"control":33,"variant_1":33,"variant_2":33}},"bExperiment":{"variants":{"control":33,"variant_1":33,"variant_2":33}}}
</script>
</amp-experiment>
</div>
`);
});

it(`should add amp-experiment extension script to page head`, async () => {
render(<AmpExperiment experimentConfig={experimentConfig} />);

await waitFor(() => {
const scripts = Array.from(document.querySelectorAll('head script'));

expect(scripts).toEqual(
expect.arrayContaining([
expect.objectContaining({
src: `https://cdn.ampproject.org/v0/amp-experiment-0.1.js`,
}),
]),
);

expect(scripts).toHaveLength(1);
});
});
});
50 changes: 50 additions & 0 deletions src/app/components/AmpExperiment/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/** @jsx jsx */
/* @jsxFrag React.Fragment */
import { jsx } from '@emotion/react';
import React from 'react';
import { Helmet } from 'react-helmet';

type Variant = string;
type Experiment = string;
type TrafficAllocationPercentage = number;

type AmpExperimentConfig = {
[key: Experiment]: {
sticky?: boolean;
consentNotificationId?: string;
variants: {
[key: Variant]: TrafficAllocationPercentage;
};
};
};

type AmpExperimentProps = {
[key: Experiment]: AmpExperimentConfig;
};

const AmpHead = () => (
<Helmet>
<script
async
custom-element="amp-experiment"
src="https://cdn.ampproject.org/v0/amp-experiment-0.1.js"
/>
</Helmet>
);

const AmpExperiment = ({ experimentConfig }: AmpExperimentProps) => {
return (
<>
<AmpHead />
<amp-experiment>
<script
type="application/json"
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: JSON.stringify(experimentConfig) }}
/>
</amp-experiment>
</>
);
};

export default AmpExperiment;
7 changes: 7 additions & 0 deletions src/app/components/AmpExperiment/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare namespace JSX {
interface IntrinsicElements {
'amp-experiment': React.PropsWithChildren<
ScriptHTMLAttributes<HTMLScriptElement>
>;
}
}
20 changes: 18 additions & 2 deletions src/app/pages/ArticlePage/ArticlePage.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ export default {
paddingBottom: `${spacings.QUADRUPLE}rem`,
},
}),

topStoriesAndFeaturesSection: ({ spacings, mq }: Theme) =>
featuresSection: ({ spacings, mq }: Theme) =>
css({
marginBottom: `${spacings.TRIPLE}rem`,

Expand All @@ -91,4 +90,21 @@ export default {
padding: `${spacings.DOUBLE}rem`,
},
}),
topStoriesSection: ({ spacings, mq }: Theme) =>
css({
marginBottom: `${spacings.TRIPLE}rem`,
[mq.GROUP_4_MIN_WIDTH]: {
display: 'block',
marginBottom: `${spacings.FULL}rem`,
padding: `${spacings.DOUBLE}rem`,
},
'[amp-x-topStoriesExperiment="show_at_halfway"] &': {
display: 'none',
[mq.GROUP_4_MIN_WIDTH]: {
display: 'block',
marginBottom: `${spacings.FULL}rem`,
padding: `${spacings.DOUBLE}rem`,
},
},
}),
};
33 changes: 28 additions & 5 deletions src/app/pages/ArticlePage/ArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,26 @@ import {
import { ServiceContext } from '../../contexts/ServiceContext';
import RelatedContentSection from '../../components/RelatedContentSection';
import Disclaimer from '../../components/Disclaimer';

import SecondaryColumn from './SecondaryColumn';

import styles from './ArticlePage.styles';
import { ComponentToRenderProps, TimeStampProps } from './types';
import AmpExperiment from '../../components/AmpExperiment';
import {
experimentTopStoriesConfig,
getExperimentTopStories,
ExperimentTopStories,
} from './experimentTopStories/helpers';

const ArticlePage = ({ pageData }: { pageData: Article }) => {
const { isApp, pageType, service } = useContext(RequestContext);
const { isApp, pageType, service, isAmp, id } = useContext(RequestContext);

const {
articleAuthor,
isTrustProjectParticipant,
showRelatedTopics,
brandName,
} = useContext(ServiceContext);

const { enabled: preloadLeadImageToggle } = useToggle('preloadLeadImage');

const {
Expand Down Expand Up @@ -131,6 +137,16 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
...(isCPS && { pageTitle: `${atiAnalytics.pageTitle} - ${brandName}` }),
};

const topStoriesContent = pageData?.secondaryColumn?.topStories;
const { shouldEnableExperimentTopStories, transformedBlocks } =
getExperimentTopStories({
blocks,
topStoriesContent,
isAmp,
service,
id,
});

const componentsToRender = {
visuallyHiddenHeadline,
headline: headings,
Expand Down Expand Up @@ -174,6 +190,10 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
<Disclaimer {...props} increasePaddingOnDesktop={false} />
),
podcastPromo: () => (podcastPromoEnabled ? <InlinePodcastPromo /> : null),
experimentTopStories: () =>
topStoriesContent ? (
<ExperimentTopStories topStoriesContent={topStoriesContent} />
) : null,
};

const visuallyHiddenBlock = {
Expand All @@ -183,8 +203,8 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
};

const articleBlocks = startsWithHeading
? blocks
: [visuallyHiddenBlock, ...blocks];
? transformedBlocks
: [visuallyHiddenBlock, ...transformedBlocks];

const promoImageBlocks =
pageData?.promo?.images?.defaultPromoImage?.blocks ?? [];
Expand All @@ -206,6 +226,9 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {

return (
<div css={styles.pageWrapper}>
{shouldEnableExperimentTopStories && (
<AmpExperiment experimentConfig={experimentTopStoriesConfig} />
)}
<ATIAnalytics atiData={atiData} />
<ChartbeatAnalytics
sectionName={pageData?.relatedContent?.section?.name}
Expand Down
7 changes: 2 additions & 5 deletions src/app/pages/ArticlePage/SecondaryColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,12 @@ const SecondaryColumn = ({ pageData }: { pageData: Article }) => {
return (
<div css={styles.secondaryColumn}>
{topStoriesContent && (
<div
css={styles.topStoriesAndFeaturesSection}
data-testid="top-stories"
>
<div css={styles.topStoriesSection} data-testid="top-stories">
<TopStoriesSection content={topStoriesContent} />
</div>
)}
{featuresContent && (
<div css={styles.topStoriesAndFeaturesSection} data-testid="features">
<div css={styles.featuresSection} data-testid="features">
<FeaturesAnalysis
content={featuresContent}
parentColumns={{}}
Expand Down
109 changes: 109 additions & 0 deletions src/app/pages/ArticlePage/experimentTopStories/helpers.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { getExperimentTopStories } from './helpers';
import { topStoriesList } from '../PagePromoSections/TopStoriesSection/fixture/index';

describe('AMP top stories experiment', () => {
const mockTextBlock = {
type: 'text',
model: {
blocks: [],
},
};
const expectedExperimentTopStoriesBlock = (index: number) => {
return {
type: 'experimentTopStories',
model: topStoriesList,
id: `experimentTopStories-${index}`,
};
};

const blocksShortLength = [mockTextBlock];

const blocksEvenLength = [
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
];
const blocksOddLength = [mockTextBlock, mockTextBlock, mockTextBlock];

describe('getExperimentTopStories()', () => {
it('returns shouldEnableExperimentTopStories as true if props match conditions.', () => {
const { shouldEnableExperimentTopStories } = getExperimentTopStories({
blocks: blocksEvenLength,
topStoriesContent: topStoriesList,
isAmp: true,
id: 'c6v11qzyv8po',
service: 'news',
});
expect(shouldEnableExperimentTopStories).toBe(true);
});

it.each`
testDescription | isAmp | id | service
${'all props are undefined'} | ${false} | ${undefined} | ${undefined}
${'only isAmp is true'} | ${true} | ${undefined} | ${undefined}
${'only pathname is undefined'} | ${true} | ${undefined} | ${'news'}
${'only pathname is defined and valid'} | ${false} | ${'c6v11qzyv8po'} | ${undefined}
${'all props defined but pathname is invalid'} | ${false} | ${'c1231qzyv8po'} | ${undefined}
${'only service is undefined'} | ${true} | ${'c6v11qzyv8po'} | ${undefined}
${'only service is defined and valid'} | ${false} | ${undefined} | ${'news'}
${'all props defined but service is invalid'} | ${true} | ${'c6v11qzyv8po'} | ${'igbo'}
`(
'returns shouldEnableExperimentTopStories as false because $testDescription.',
({ isAmp, id, service }) => {
const { shouldEnableExperimentTopStories } = getExperimentTopStories({
blocks: blocksEvenLength,
topStoriesContent: topStoriesList,
isAmp,
id,
service,
});

expect(shouldEnableExperimentTopStories).toBe(false);
},
);

const expectedBlocksEvenLength = [
mockTextBlock,
mockTextBlock,
expectedExperimentTopStoriesBlock(2),
mockTextBlock,
mockTextBlock,
];
const expectedBlocksOddLength = [
mockTextBlock,
expectedExperimentTopStoriesBlock(1),
mockTextBlock,
mockTextBlock,
];

it.each`
testType | inputBlocks | expectedOutput
${'even'} | ${blocksEvenLength} | ${expectedBlocksEvenLength}
${'odd'} | ${blocksOddLength} | ${expectedBlocksOddLength}
`(
'should insert experimentTopStories block into blocks array in the correct position when blocks.length is $testType',
({ inputBlocks, expectedOutput }) => {
const { transformedBlocks } = getExperimentTopStories({
blocks: inputBlocks,
topStoriesContent: topStoriesList,
isAmp: true,
id: 'c6v11qzyv8po',
service: 'news',
});
expect(transformedBlocks).toEqual(expectedOutput);
},
);

it('does not insert experiment top stories blocks if the blocks array length is < 2.', () => {
const { transformedBlocks } = getExperimentTopStories({
blocks: blocksShortLength,
topStoriesContent: topStoriesList,
isAmp: true,
id: 'c6v11qzyv8po',
service: 'news',
});
expect(transformedBlocks).toBe(blocksShortLength);
});
});
});
Loading

0 comments on commit 4fcac7c

Please sign in to comment.