Skip to content

Commit 50a0604

Browse files
Merge branch 'main' into filter/carousel-card
2 parents 9d936d6 + fc806f3 commit 50a0604

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1515
-71
lines changed

ab-testing/config/abTests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ const ABTests: ABTest[] = [
2424
name: "commercial-prebid-v10",
2525
description: "Testing Prebid.js v10 integration on DCR",
2626
owners: ["[email protected]"],
27-
status: "OFF",
28-
expirationDate: "2025-12-10",
27+
status: "ON",
28+
expirationDate: "2026-01-10",
2929
type: "client",
3030
audienceSize: 10 / 100,
3131
audienceSpace: "A",

dotcom-rendering/.storybook/preview.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ import { Picture } from '../src/components/Picture';
1212
import { mockFetch } from '../src/lib/mockRESTCalls';
1313
import { setABTests } from '../src/lib/useAB';
1414
import { ConfigContextDecorator } from './decorators/configContextDecorator';
15+
import { sb } from 'storybook/test';
1516
import { Preview } from '@storybook/react-webpack5';
1617
import {
1718
globalColourScheme,
1819
globalColourSchemeDecorator,
1920
} from './toolbar/globalColourScheme';
2021
import { palette as sourcePalette } from '@guardian/source/foundations';
2122

23+
// Set up module mocking for auth and newsletter subscription hooks
24+
sb.mock(import('../src/lib/useNewsletterSubscription.ts'), { spy: true });
25+
sb.mock(import('../src/lib/useAuthStatus.ts'), { spy: true });
26+
sb.mock(import('../src/lib/fetchEmail.ts'), { spy: true });
27+
2228
// Prevent components being lazy rendered when we're taking Chromatic snapshots
2329
Lazy.disabled = isChromatic();
2430
Picture.disableLazyLoading = isChromatic();
@@ -64,6 +70,7 @@ style.appendChild(document.createTextNode(css));
6470
},
6571
page: {
6672
ajaxUrl: 'https://api.nextgen.guardianapps.co.uk',
73+
idApiUrl: 'https://idapi.theguardian.com',
6774
},
6875
tests: {},
6976
switches: {},

dotcom-rendering/docs/development/ab-testing-in-dcr.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,20 @@ These links are also in the [frontend admin](https://frontend.gutools.co.uk/anal
255255
- Opt-out on PROD: `https://theguardian.com/ab-tests/opt/out`
256256

257257
You can use the same routes on CODE.
258+
259+
### 6. Forcing yourself into a test locally
260+
261+
Use the opt-in and opt-out URL fragments to force yourself into or out of a test using a query parameter.
262+
263+
When opted-in, the test will override any mvt based assignment and you'll only be in the opted-in test group.
264+
265+
**Opt-in Example**
266+
267+
- Articles: `http://localhost:3030/Article/?ab-commercial-test-example=variant`
268+
- Fronts: `http://localhost:3030/Front/https://www.theguardian.com/international?ab-commercial-test-example=variant`
269+
- Interactives: `http://localhost:3030/Interactive/https://www.theguardian.com/global-development/ng-interactive/2022/jun/09/the-black-sea-blockade-mapping-the-impact-of-war-in-ukraine-on-the-worlds-food-supply-interactive?ab--commercial-test-example=variant`
270+
271+
You can verify that you're in the test by checking:
272+
273+
- Server-side tests: `window.guardian.config.serverSideABTests` in the browser console
274+
- Client-side tests: `window.guardian.modules.abTests.getParticipations()` in the browser console

dotcom-rendering/fixtures/generated/match-report.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export const matchReport: MatchReportType = {
274274
shotsOff: 6,
275275
corners: 10,
276276
fouls: 4,
277-
colours: '#ffffff',
277+
colours: '#01009a',
278278
crest: 'https://sport.guim.co.uk/football/crests/120/7699.png',
279279
},
280280
awayTeam: {

dotcom-rendering/src/components/ArticleBody.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type Props = {
5555
isRightToLeftLang?: boolean;
5656
shouldHideAds: boolean;
5757
serverTime?: number;
58+
idApiUrl?: string;
5859
};
5960

6061
const globalOlStyles = () => css`
@@ -139,6 +140,7 @@ export const ArticleBody = ({
139140
editionId,
140141
shouldHideAds,
141142
serverTime,
143+
idApiUrl,
142144
}: Props) => {
143145
const isInteractiveContent =
144146
format.design === ArticleDesign.Interactive ||
@@ -208,6 +210,7 @@ export const ArticleBody = ({
208210
editionId={editionId}
209211
shouldHideAds={shouldHideAds}
210212
serverTime={serverTime}
213+
idApiUrl={idApiUrl}
211214
/>
212215
</div>
213216
);
@@ -256,6 +259,7 @@ export const ArticleBody = ({
256259
editionId={editionId}
257260
contributionsServiceUrl={contributionsServiceUrl}
258261
shouldHideAds={shouldHideAds}
262+
idApiUrl={idApiUrl}
259263
/>
260264
</div>
261265
{hasObserverPublicationTag && <ObserverFooter />}

dotcom-rendering/src/components/Caption.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export const Caption = ({
315315
]}
316316
data-spacefinder-role="inline"
317317
>
318-
{mediaType === 'YoutubeVideo' ? (
318+
{mediaType === 'YoutubeVideo' || mediaType === 'SelfHostedVideo' ? (
319319
<VideoIcon format={format} />
320320
) : (
321321
<CameraIcon format={format} />
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { Breakpoint } from '@guardian/source/foundations';
2+
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
3+
import type { EmailSignUpProps } from './EmailSignup';
4+
import { EmailSignup } from './EmailSignup';
5+
import { InlineSkipToWrapper } from './InlineSkipToWrapper';
6+
import { Island } from './Island';
7+
import { NewsletterPrivacyMessage } from './NewsletterPrivacyMessage';
8+
import { Placeholder } from './Placeholder';
9+
import { SecureSignup } from './SecureSignup.importable';
10+
11+
/**
12+
* Approximate heights of the EmailSignup component at different breakpoints.
13+
*/
14+
const PLACEHOLDER_HEIGHTS = new Map<Breakpoint, number>([
15+
['mobile', 220],
16+
['tablet', 180],
17+
['desktop', 180],
18+
]);
19+
20+
interface EmailSignUpWrapperProps extends EmailSignUpProps {
21+
index: number;
22+
listId: number;
23+
identityName: string;
24+
successDescription: string;
25+
idApiUrl: string;
26+
/** You should only set this to true if the privacy message will be shown elsewhere on the page */
27+
hidePrivacyMessage?: boolean;
28+
}
29+
30+
/**
31+
* EmailSignUpWrapper as an importable island component.
32+
*
33+
* This component needs to be hydrated client-side because it uses
34+
* the useNewsletterSubscription hook which depends on auth status
35+
* to determine if the user is already subscribed to the newsletter.
36+
*
37+
* If the user is signed in and already subscribed, this component
38+
* will return null (hide the signup form).
39+
*/
40+
export const EmailSignUpWrapper = ({
41+
index,
42+
listId,
43+
idApiUrl,
44+
...emailSignUpProps
45+
}: EmailSignUpWrapperProps) => {
46+
const isSubscribed = useNewsletterSubscription(listId, idApiUrl);
47+
48+
// Show placeholder while subscription status is being determined
49+
// This prevents layout shift in both subscribed and non-subscribed cases
50+
if (isSubscribed === undefined) {
51+
return <Placeholder heights={PLACEHOLDER_HEIGHTS} />;
52+
}
53+
54+
// Don't render if user is signed in and already subscribed
55+
if (isSubscribed) {
56+
return null;
57+
}
58+
59+
return (
60+
<InlineSkipToWrapper
61+
id={`EmailSignup-skip-link-${index}`}
62+
blockDescription="newsletter promotion"
63+
>
64+
<EmailSignup {...emailSignUpProps}>
65+
<Island priority="feature" defer={{ until: 'visible' }}>
66+
<SecureSignup
67+
newsletterId={emailSignUpProps.identityName}
68+
successDescription={emailSignUpProps.description}
69+
/>
70+
</Island>
71+
{!emailSignUpProps.hidePrivacyMessage && (
72+
<NewsletterPrivacyMessage />
73+
)}
74+
</EmailSignup>
75+
</InlineSkipToWrapper>
76+
);
77+
};
Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,96 @@
11
import type { Meta, StoryObj } from '@storybook/react-webpack5';
2-
import { EmailSignUpWrapper } from './EmailSignUpWrapper';
2+
import { mocked } from 'storybook/test';
3+
import { lazyFetchEmailWithTimeout } from '../lib/fetchEmail';
4+
import { useIsSignedIn } from '../lib/useAuthStatus';
5+
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
6+
import { EmailSignUpWrapper } from './EmailSignUpWrapper.importable';
37

48
const meta: Meta<typeof EmailSignUpWrapper> = {
59
title: 'Components/EmailSignUpWrapper',
610
component: EmailSignUpWrapper,
711
};
812

13+
type Story = StoryObj<typeof EmailSignUpWrapper>;
14+
915
const defaultArgs = {
1016
index: 10,
17+
listId: 4147,
1118
identityName: 'the-recap',
1219
description:
13-
'The best of our sports journalism from the past seven days and a heads-up on the weekends action',
20+
"The best of our sports journalism from the past seven days and a heads-up on the weekend's action",
1421
name: 'The Recap',
1522
frequency: 'Weekly',
1623
successDescription: "We'll send you The Recap every week",
1724
theme: 'sport',
25+
idApiUrl: 'https://idapi.theguardian.com',
1826
} satisfies Story['args'];
19-
type Story = StoryObj<typeof EmailSignUpWrapper>;
2027

28+
// Loading state - shows placeholder while auth status is being determined
29+
// This prevents layout shift when subscription status is resolved
30+
export const Placeholder: Story = {
31+
args: {
32+
hidePrivacyMessage: false,
33+
...defaultArgs,
34+
},
35+
async beforeEach() {
36+
mocked(useNewsletterSubscription).mockReturnValue(undefined);
37+
},
38+
};
39+
40+
// Default story - signed out user sees the signup form with email input
2141
export const DefaultStory: Story = {
2242
args: {
2343
hidePrivacyMessage: true,
2444
...defaultArgs,
2545
},
46+
async beforeEach() {
47+
mocked(useNewsletterSubscription).mockReturnValue(false);
48+
mocked(useIsSignedIn).mockReturnValue(false);
49+
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
50+
Promise.resolve(null),
51+
);
52+
},
2653
};
2754

2855
export const DefaultStoryWithPrivacy: Story = {
2956
args: {
3057
hidePrivacyMessage: false,
3158
...defaultArgs,
3259
},
60+
async beforeEach() {
61+
mocked(useNewsletterSubscription).mockReturnValue(false);
62+
mocked(useIsSignedIn).mockReturnValue(false);
63+
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
64+
Promise.resolve(null),
65+
);
66+
},
67+
};
68+
69+
// User is signed in but NOT subscribed - email field is hidden, only signup button shows
70+
export const SignedInNotSubscribed: Story = {
71+
args: {
72+
hidePrivacyMessage: false,
73+
...defaultArgs,
74+
},
75+
async beforeEach() {
76+
mocked(useNewsletterSubscription).mockReturnValue(false);
77+
mocked(useIsSignedIn).mockReturnValue(true);
78+
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
79+
Promise.resolve('[email protected]'),
80+
);
81+
},
82+
};
83+
84+
// User is signed in and IS subscribed - component returns null (hidden)
85+
// Note: This story will render nothing as the component returns null when subscribed
86+
export const SignedInAlreadySubscribed: Story = {
87+
args: {
88+
hidePrivacyMessage: false,
89+
...defaultArgs,
90+
},
91+
async beforeEach() {
92+
mocked(useNewsletterSubscription).mockReturnValue(true);
93+
},
3394
};
3495

3596
export default meta;

dotcom-rendering/src/components/EmailSignUpWrapper.tsx

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)