Skip to content

Commit f694cef

Browse files
authored
Merge branch 'main' into ei/skimlinks-xcust-app
2 parents 98de1c9 + 66a78d8 commit f694cef

29 files changed

+560
-233
lines changed

dotcom-rendering/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@guardian/shimport": "1.0.2",
4444
"@guardian/source": "9.0.0",
4545
"@guardian/source-development-kitchen": "18.1.1",
46-
"@guardian/support-dotcom-components": "7.5.0",
46+
"@guardian/support-dotcom-components": "7.6.2",
4747
"@guardian/tsconfig": "0.2.0",
4848
"@playwright/test": "1.52.0",
4949
"@sentry/browser": "7.75.1",

dotcom-rendering/src/components/FrontSection.tsx

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,40 @@ type Props = {
9898
const width = (columns: number, columnWidth: number, columnGap: number) =>
9999
`width: ${columns * columnWidth + (columns - 1) * columnGap}px;`;
100100

101+
const borderColourStyles = (title?: string): string => {
102+
switch (title) {
103+
case 'News':
104+
return schemePalette('--section-border-news');
105+
case 'Opinion':
106+
return schemePalette('--section-border-opinion');
107+
case 'Sport':
108+
return schemePalette('--section-border-sport');
109+
case 'Lifestyle':
110+
return schemePalette('--section-border-lifestyle');
111+
case 'Culture':
112+
return schemePalette('--section-border-culture');
113+
default:
114+
return schemePalette('--section-border-primary');
115+
}
116+
};
117+
118+
const articleSectionTitleStyles = (title?: string): string => {
119+
switch (title) {
120+
case 'News':
121+
return schemePalette('--article-section-title-news');
122+
case 'Opinion':
123+
return schemePalette('--article-section-title-opinion');
124+
case 'Sport':
125+
return schemePalette('--article-section-title-sport');
126+
case 'Lifestyle':
127+
return schemePalette('--article-section-title-lifestyle');
128+
case 'Culture':
129+
return schemePalette('--article-section-title-culture');
130+
default:
131+
return schemePalette('--article-section-title');
132+
}
133+
};
134+
101135
/** Not all browsers support CSS grid, so we set explicit width as a fallback */
102136
const fallbackStyles = css`
103137
@supports not (display: grid) {
@@ -401,10 +435,10 @@ const bottomPaddingBetaContainer = (
401435
}
402436
`;
403437

404-
const primaryLevelTopBorder = css`
438+
const primaryLevelTopBorder = (title?: string) => css`
405439
grid-row: 1;
406440
grid-column: 1 / -1;
407-
border-top: 2px solid ${schemePalette('--section-border-primary')};
441+
border-top: 2px solid ${borderColourStyles(title)};
408442
/** Ensures the top border sits above the side borders */
409443
z-index: 1;
410444
height: fit-content;
@@ -590,7 +624,7 @@ export const FrontSection = ({
590624
css={[
591625
containerLevel === 'Secondary'
592626
? secondaryLevelTopBorder
593-
: primaryLevelTopBorder,
627+
: primaryLevelTopBorder(title),
594628
]}
595629
/>
596630
)}
@@ -629,9 +663,7 @@ export const FrontSection = ({
629663
? schemePalette(
630664
'--article-section-secondary-title',
631665
)
632-
: schemePalette(
633-
'--article-section-title',
634-
)
666+
: articleSectionTitleStyles(title)
635667
}
636668
// On paid fronts the title is not treated as a link
637669
url={!isOnPaidContentFront ? url : undefined}

dotcom-rendering/src/components/FrontsAdSlots.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export const MerchandisingSlot = ({
6464
renderAds && (
6565
<Section
6666
fullWidth={true}
67-
data-print-layout="hide"
6867
padSides={false}
6968
showTopBorder={false}
7069
showSideBorders={false}

dotcom-rendering/src/components/LoopVideo.stories.tsx

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { breakpoints } from '@guardian/source/foundations';
22
import type { Meta, StoryObj } from '@storybook/react';
3+
import { expect, userEvent, within } from '@storybook/test';
34
import { centreColumnDecorator } from '../../.storybook/decorators/gridDecorators';
45
import { LoopVideo } from './LoopVideo.importable';
56

6-
export default {
7+
const meta = {
78
component: LoopVideo,
89
title: 'Components/LoopVideo',
910
decorators: [centreColumnDecorator],
@@ -15,7 +16,10 @@ export default {
1516
},
1617
} satisfies Meta<typeof LoopVideo>;
1718

18-
export const Default = {
19+
export default meta;
20+
type Story = StoryObj<typeof LoopVideo>;
21+
22+
export const Default: Story = {
1923
name: 'Default',
2024
args: {
2125
src: 'https://uploads.guim.co.uk/2025%2F06%2F20%2Ftesting+only%2C+please+ignore--3cb22b60-2c3f-48d6-8bce-38c956907cce-3.mp4',
@@ -26,14 +30,84 @@ export const Default = {
2630
image: 'https://media.guim.co.uk/9bdb802e6da5d3fd249b5060f367b3a817965f0c/0_0_1800_1080/master/1800.jpg',
2731
fallbackImage: '',
2832
},
29-
} satisfies StoryObj<typeof LoopVideo>;
33+
};
3034

31-
export const Without5to4Ratio = {
35+
export const Without5to4Ratio: Story = {
3236
name: 'Without 5:4 aspect ratio',
3337
args: {
3438
...Default.args,
3539
src: 'https://uploads.guim.co.uk/2024/10/01/241001HeleneLoop_2.mp4',
3640
height: 1080,
3741
width: 1920,
3842
},
39-
} satisfies StoryObj<typeof LoopVideo>;
43+
};
44+
45+
export const PausePlay: Story = {
46+
...Default,
47+
play: async ({ canvasElement }) => {
48+
const canvas = within(canvasElement);
49+
const videoEl = canvas.getByTestId('loop-video');
50+
51+
await userEvent.click(videoEl, {
52+
delay: 300, // Allow video to start playing.
53+
});
54+
await canvas.findByTestId('play-icon');
55+
56+
const progressBar = await canvas.findByRole('progressbar');
57+
await expect(Number(progressBar.ariaValueNow)).toBeGreaterThan(0);
58+
59+
// Play Video
60+
await userEvent.click(videoEl);
61+
await expect(canvas.queryByTestId('play-icon')).not.toBeInTheDocument();
62+
},
63+
};
64+
65+
export const UnmuteMute: Story = {
66+
...Default,
67+
parameters: {
68+
test: {
69+
// The following error is received without this flag: "TypeError: ophan.trackClickComponentEvent is not a function"
70+
dangerouslyIgnoreUnhandledErrors: true,
71+
},
72+
},
73+
play: async ({ canvasElement }) => {
74+
const canvas = within(canvasElement);
75+
76+
await canvas.findByTestId('unmute-icon');
77+
78+
await userEvent.click(canvas.getByTestId('unmute-icon'));
79+
await canvas.findByTestId('mute-icon');
80+
81+
await userEvent.click(canvas.getByTestId('mute-icon'));
82+
await canvas.findByTestId('unmute-icon');
83+
},
84+
};
85+
86+
// Function to emulate pausing between interactions
87+
function sleep(ms: number) {
88+
return new Promise((resolve) => setTimeout(resolve, ms));
89+
}
90+
91+
export const InteractionObserver: Story = {
92+
...Default,
93+
render: (args) => (
94+
<div data-testid="test-container">
95+
<LoopVideo {...args} />
96+
<div style={{ height: '100vh' }}></div>
97+
<p data-testid="page-end">End of page</p>
98+
</div>
99+
),
100+
play: async ({ canvasElement }) => {
101+
const canvas = within(canvasElement);
102+
103+
await sleep(500); // Allow enough time for the autoplay video to start.
104+
canvas.getByTestId('page-end').scrollIntoView();
105+
106+
const progressBar = await canvas.findByRole('progressbar');
107+
const progress = Number(progressBar.ariaValueNow);
108+
109+
await sleep(500); // Allow enough time to be confident that the video is paused.
110+
await expect(Number(progressBar.ariaValueNow)).toEqual(progress);
111+
await expect(canvas.queryByTestId('play-icon')).not.toBeInTheDocument();
112+
},
113+
};

dotcom-rendering/src/components/LoopVideoPlayer.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ export const LoopVideoPlayer = forwardRef(
137137
id={loopVideoId}
138138
css={videoStyles(width, height)}
139139
ref={ref}
140-
role="button"
141140
tabIndex={0}
141+
data-testid="loop-video"
142142
height={height}
143143
width={width}
144144
data-link-name={`gu-video-loop-${
@@ -180,6 +180,7 @@ export const LoopVideoPlayer = forwardRef(
180180
onClick={handlePlayPauseClick}
181181
css={playIconStyles}
182182
data-link-name={`gu-video-loop-play-${atomId}`}
183+
data-testid="play-icon"
183184
>
184185
<PlayIcon iconWidth="narrow" />
185186
</button>
@@ -199,7 +200,12 @@ export const LoopVideoPlayer = forwardRef(
199200
isMuted ? 'unmute' : 'mute'
200201
}-${atomId}`}
201202
>
202-
<div css={audioIconContainerStyles}>
203+
<div
204+
css={audioIconContainerStyles}
205+
data-testId={`${
206+
isMuted ? 'unmute' : 'mute'
207+
}-icon`}
208+
>
203209
<AudioIcon
204210
size="xsmall"
205211
theme={{

dotcom-rendering/src/components/marketing/banners/designableBanner/DesignableBanner.tsx

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
} from '../../../../lib/useMatchMedia';
3131
import { getChoiceCards } from '../../lib/choiceCards';
3232
import type { ReactComponent } from '../../lib/ReactComponent';
33-
import { addChoiceCardsProductParams } from '../../lib/tracking';
33+
import { getChoiceCardUrl } from '../../lib/tracking';
3434
import { ThreeTierChoiceCards } from '../../shared/ThreeTierChoiceCards';
3535
import { bannerWrapper, validatedBannerWrapper } from '../common/BannerWrapper';
3636
import type { BannerRenderProps } from '../common/types';
@@ -108,19 +108,6 @@ const buildChoiceCardSettings = (
108108
return undefined;
109109
};
110110

111-
const buildUrlForThreeTierChoiceCards = (
112-
baseUrl: string,
113-
selectedProduct: ChoiceCard['product'],
114-
) => {
115-
return selectedProduct.supportTier === 'OneOff'
116-
? baseUrl
117-
: addChoiceCardsProductParams(
118-
baseUrl,
119-
selectedProduct.supportTier,
120-
selectedProduct.ratePlan,
121-
);
122-
};
123-
124111
const DesignableBanner: ReactComponent<BannerRenderProps> = ({
125112
content,
126113
onCloseClick,
@@ -160,11 +147,11 @@ const DesignableBanner: ReactComponent<BannerRenderProps> = ({
160147
}, [iosAppBannerPresent, submitComponentEvent]);
161148

162149
const choiceCards = getChoiceCards(isTabletOrAbove, choiceCardsSettings);
163-
const defaultProduct = choiceCards?.find((cc) => cc.isDefault)?.product;
164-
const [
165-
threeTierChoiceCardSelectedProduct,
166-
setThreeTierChoiceCardSelectedProduct,
167-
] = useState<ChoiceCard['product'] | undefined>(defaultProduct);
150+
const defaultChoiceCard = choiceCards?.find((cc) => cc.isDefault);
151+
152+
const [selectedChoiceCard, setSelectedChoiceCard] = useState<
153+
ChoiceCard | undefined
154+
>(defaultChoiceCard);
168155

169156
// We can't render anything without a design
170157
if (!design) {
@@ -379,7 +366,7 @@ const DesignableBanner: ReactComponent<BannerRenderProps> = ({
379366
</div>
380367
)}
381368

382-
{!threeTierChoiceCardSelectedProduct && (
369+
{!selectedChoiceCard && (
383370
<div css={styles.outerImageCtaContainer}>
384371
<div css={styles.innerImageCtaContainer}>
385372
<DesignableBannerCtas
@@ -405,25 +392,21 @@ const DesignableBanner: ReactComponent<BannerRenderProps> = ({
405392
</div>
406393

407394
{choiceCards &&
408-
threeTierChoiceCardSelectedProduct &&
395+
selectedChoiceCard &&
409396
mainOrMobileContent.primaryCta && (
410397
<div css={styles.threeTierChoiceCardsContainer}>
411398
<ThreeTierChoiceCards
412-
selectedProduct={
413-
threeTierChoiceCardSelectedProduct
414-
}
415-
setSelectedProduct={
416-
setThreeTierChoiceCardSelectedProduct
417-
}
399+
selectedChoiceCard={selectedChoiceCard}
400+
setSelectedChoiceCard={setSelectedChoiceCard}
418401
choices={choiceCards}
419402
id={'banner'}
420403
/>
421404

422405
<div css={styles.ctaContainer}>
423406
<LinkButton
424-
href={buildUrlForThreeTierChoiceCards(
407+
href={getChoiceCardUrl(
408+
selectedChoiceCard,
425409
mainOrMobileContent.primaryCta.ctaUrl,
426-
threeTierChoiceCardSelectedProduct,
427410
)}
428411
onClick={onCtaClick}
429412
priority="tertiary"

dotcom-rendering/src/components/marketing/banners/designableBanner/stories/DesignableBanner.stories.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type {
55
} from '@guardian/support-dotcom-components/dist/shared/types';
66
import type { Meta, StoryObj } from '@storybook/react';
77
import lzstring from 'lz-string';
8-
import { choiceCardsSettings } from '../../../lib/storybook';
8+
import {
9+
choiceCardsSettings,
10+
choiceCardsWithDestinationUrl,
11+
choiceCardsWithDestinationUrlTwoCards,
12+
} from '../../../lib/storybook';
913
import {
1014
contentNoHeading,
1115
design,
@@ -281,3 +285,57 @@ export const NoChoiceCardOrImage: Story = {
281285
},
282286
},
283287
};
288+
289+
export const WithDestinationUrlAllCards: Story = {
290+
name: 'With destinationUrl on all choice cards',
291+
args: {
292+
...meta.args,
293+
design: {
294+
...design,
295+
visual: {
296+
kind: 'ChoiceCards',
297+
buttonColour: stringToHexColour('E5E5E5'),
298+
},
299+
},
300+
tracking: {
301+
...tracking,
302+
abTestVariant: 'THREE_TIER_CHOICE_CARDS',
303+
},
304+
choiceCardAmounts: regularChoiceCardAmounts,
305+
choiceCardsSettings: choiceCardsWithDestinationUrl,
306+
},
307+
parameters: {
308+
docs: {
309+
description: {
310+
story: 'All choice cards have a destinationUrl configured. The banner should use these custom URLs instead of constructing URLs with product parameters.',
311+
},
312+
},
313+
},
314+
};
315+
316+
export const WithDestinationUrlTwoCards: Story = {
317+
name: 'With destinationUrl in two choice cards',
318+
args: {
319+
...meta.args,
320+
design: {
321+
...design,
322+
visual: {
323+
kind: 'ChoiceCards',
324+
buttonColour: stringToHexColour('E5E5E5'),
325+
},
326+
},
327+
tracking: {
328+
...tracking,
329+
abTestVariant: 'THREE_TIER_CHOICE_CARDS',
330+
},
331+
choiceCardAmounts: regularChoiceCardAmounts,
332+
choiceCardsSettings: choiceCardsWithDestinationUrlTwoCards,
333+
},
334+
parameters: {
335+
docs: {
336+
description: {
337+
story: 'All choice cards have a destinationUrl configured. The banner should use these custom URLs instead of constructing URLs with product parameters.',
338+
},
339+
},
340+
},
341+
};

0 commit comments

Comments
 (0)