Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions ab-testing/config/abTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,6 @@ const ABTests: ABTest[] = [
groups: ["control", "variant"],
shouldForceMetricsCollection: true,
},
{
name: "newsletters-in-article-signup-preview",
description:
"Test in-article newsletter signup with illustrated preview CTA vs without preview CTA",
owners: ["newsletters.dev@guardian.co.uk"],
expirationDate: "2026-07-21",
type: "client",
status: "ON",
audienceSize: 50 / 100,
audienceSpace: "A",
groups: ["illustrated", "without-preview"],
shouldForceMetricsCollection: false,
},
{
name: "fronts-and-curation-loop-click-through",
description:
Expand Down
118 changes: 5 additions & 113 deletions dotcom-rendering/src/components/EmailSignUpWrapper.island.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { submitComponentEvent } from '../client/ophan/ophan';
import {
NEWSLETTER_PREVIEW_AB_TEST_NAME,
NEWSLETTER_PREVIEW_VARIANT,
} from '../lib/newsletterSignupAbTest';
import { NEWSLETTER_SIGNUP_COMPONENT_ID } from '../lib/newsletterSignupTracking';
import { useAB } from '../lib/useAB';
import { useIsSignedIn } from '../lib/useAuthStatus';
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
import { ConfigProvider } from './ConfigContext';
Expand All @@ -24,42 +19,23 @@ jest.mock('../lib/useNewsletterSubscription', () => ({
useNewsletterSubscription: jest.fn(),
}));

jest.mock('../lib/useAB', () => ({
useAB: jest.fn(),
}));

// Avoid rendering real island children in unit tests
jest.mock('./Island', () => ({
Island: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

const mockNewsletterSignupForm = jest.fn();

jest.mock('./NewsletterSignupForm.island', () => ({
NewsletterSignupForm: (props: unknown) => {
mockNewsletterSignupForm(props);
return (
<div data-testid="newsletter-signup-form">NewsletterSignupForm</div>
);
},
NewsletterSignupForm: () => (
<div data-testid="newsletter-signup-form">NewsletterSignupForm</div>
),
}));

const mockNewsletterSignupCardContainer = jest.fn();

jest.mock('./NewsletterSignupCardContainer', () => ({
NewsletterSignupCardContainer: ({
children,
...props
}: {
children: (openPreview: (() => void) | undefined) => React.ReactNode;
}) => {
mockNewsletterSignupCardContainer(props);
return (
<div data-testid="newsletter-signup-card-container">
{children(undefined)}
</div>
);
},
children: React.ReactNode;
}) => <div data-testid="newsletter-signup-card-container">{children}</div>,
}));

const defaultProps = {
Expand Down Expand Up @@ -92,12 +68,6 @@ describe('EmailSignUpWrapper', () => {
jest.resetAllMocks();
(useIsSignedIn as jest.Mock).mockReturnValue(false);
(useNewsletterSubscription as jest.Mock).mockReturnValue(false);
(useAB as jest.Mock).mockReturnValue({
getParticipations: () => ({
[NEWSLETTER_PREVIEW_AB_TEST_NAME]:
NEWSLETTER_PREVIEW_VARIANT.illustrated,
}),
});
});

describe('rendering', () => {
Expand Down Expand Up @@ -151,58 +121,6 @@ describe('EmailSignUpWrapper', () => {
expect(submitComponentEvent).toHaveBeenCalledTimes(1);
});

it('passes AB metadata and keeps preview enabled in the illustrated arm', () => {
renderWrapper();

expect(mockNewsletterSignupCardContainer).toHaveBeenCalledWith(
expect.objectContaining({
enablePreview: true,
abTest: {
name: NEWSLETTER_PREVIEW_AB_TEST_NAME,
variant: NEWSLETTER_PREVIEW_VARIANT.illustrated,
},
}),
);
expect(mockNewsletterSignupForm).toHaveBeenCalledWith(
expect.objectContaining({
abTest: {
name: NEWSLETTER_PREVIEW_AB_TEST_NAME,
variant: NEWSLETTER_PREVIEW_VARIANT.illustrated,
},
}),
);
expect(submitComponentEvent).toHaveBeenCalledWith(
expect.objectContaining({
abTest: {
name: NEWSLETTER_PREVIEW_AB_TEST_NAME,
variant: NEWSLETTER_PREVIEW_VARIANT.illustrated,
},
}),
'Web',
);
});

it('disables preview in the without-preview arm', () => {
(useAB as jest.Mock).mockReturnValue({
getParticipations: () => ({
[NEWSLETTER_PREVIEW_AB_TEST_NAME]:
NEWSLETTER_PREVIEW_VARIANT.withoutPreview,
}),
});

renderWrapper();

expect(mockNewsletterSignupCardContainer).toHaveBeenCalledWith(
expect.objectContaining({
enablePreview: false,
abTest: {
name: NEWSLETTER_PREVIEW_AB_TEST_NAME,
variant: NEWSLETTER_PREVIEW_VARIANT.withoutPreview,
},
}),
);
});

it('does not fire a VIEW event while subscription status is loading', () => {
(useNewsletterSubscription as jest.Mock).mockReturnValue(undefined);
renderWrapper();
Expand All @@ -216,31 +134,5 @@ describe('EmailSignUpWrapper', () => {

expect(submitComponentEvent).not.toHaveBeenCalled();
});

it('still fires the VIEW event without AB metadata if the AB framework never resolves', () => {
jest.useFakeTimers();

try {
(useAB as jest.Mock).mockReturnValue(undefined);

renderWrapper();

// The VIEW event is deferred while waiting for the AB framework.
expect(submitComponentEvent).not.toHaveBeenCalled();

jest.advanceTimersByTime(2000);

expect(submitComponentEvent).toHaveBeenCalledTimes(1);
expect(submitComponentEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'VIEW',
abTest: undefined,
}),
'Web',
);
} finally {
jest.useRealTimers();
}
});
});
});
100 changes: 20 additions & 80 deletions dotcom-rendering/src/components/EmailSignUpWrapper.island.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { useEffect, useRef } from 'react';
import {
isWithoutPreviewVariant,
NEWSLETTER_PREVIEW_AB_TEST_NAME,
resolveNewsletterPreviewAbTest,
} from '../lib/newsletterSignupAbTest';
import {
NEWSLETTER_SIGNUP_COMPONENT_ID,
sendNewsletterSignupEvent,
} from '../lib/newsletterSignupTracking';
import { useAB } from '../lib/useAB';
import { useIsSignedIn } from '../lib/useAuthStatus';
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
import { useConfig } from './ConfigContext';
Expand All @@ -18,22 +12,13 @@ import { Island } from './Island';
import { NewsletterSignupCardContainer } from './NewsletterSignupCardContainer';
import { NewsletterSignupForm } from './NewsletterSignupForm.island';

/**
* How long to wait for the AB framework to resolve before firing the VIEW
* event without test metadata. Ensures newsletter view tracking still fires
* even if the AB framework never initialises.
*/
const AB_RESOLUTION_TIMEOUT_MS = 2000;

interface EmailSignUpWrapperProps extends EmailSignUpProps {
index: number;
listId: number;
identityName: string;
category?: string;
/** Illustration image URL (square crop) for the NewsletterSignupCard */
illustrationSquare?: string;
idApiUrl: string;
exampleUrl?: string;
}

/**
Expand All @@ -46,24 +31,16 @@ export const EmailSignUpWrapper = ({
index,
listId,
identityName,
category,
idApiUrl,
exampleUrl,
name,
description,
illustrationSquare,
frequency,
theme,
}: EmailSignUpWrapperProps) => {
const { renderingTarget } = useConfig();
const abTests = useAB();
const isSignedIn = useIsSignedIn();
const isSubscribed = useNewsletterSubscription(listId, idApiUrl);
const isABResolved = abTests !== undefined;
const previewVariant =
abTests?.getParticipations()[NEWSLETTER_PREVIEW_AB_TEST_NAME];
const abTest = resolveNewsletterPreviewAbTest(previewVariant);
const enablePreview = !isWithoutPreviewVariant(previewVariant);

const componentId =
NEWSLETTER_SIGNUP_COMPONENT_ID.inArticleSignupForm(identityName);
Expand All @@ -83,44 +60,17 @@ export const EmailSignUpWrapper = ({
if (viewFiredRef.current) {
return;
}

const fireView = () => {
if (viewFiredRef.current) {
return;
}
viewFiredRef.current = true;
sendNewsletterSignupEvent({
action: 'VIEW',
identityName,
componentId,
renderingTarget,
abTest,
value: {
eventDescription: 'newsletter-signup-viewed',
},
});
};

// When the AB framework has resolved, fire immediately with the test
// metadata attached. Otherwise wait briefly for it to resolve so we can
// attribute the view to the correct arm — but never block the VIEW event
// indefinitely: the AB framework can fail to initialise, and newsletter
// tracking must continue to work regardless.
if (isABResolved) {
fireView();
return;
}

const timeoutId = setTimeout(fireView, AB_RESOLUTION_TIMEOUT_MS);
return () => clearTimeout(timeoutId);
}, [
abTest,
componentId,
identityName,
isABResolved,
isSubscribed,
renderingTarget,
]);
viewFiredRef.current = true;
sendNewsletterSignupEvent({
action: 'VIEW',
identityName,
componentId,
renderingTarget,
value: {
eventDescription: 'newsletter-signup-viewed',
},
});
}, [componentId, identityName, isSubscribed, renderingTarget]);

return (
<InlineSkipToWrapper
Expand All @@ -133,27 +83,17 @@ export const EmailSignUpWrapper = ({
illustrationSquare={illustrationSquare}
frequency={frequency}
theme={theme}
identityName={identityName}
category={category}
exampleUrl={exampleUrl}
renderingTarget={renderingTarget}
abTest={abTest}
enablePreview={enablePreview}
isSignedIn={isSignedIn}
>
{(previewAction) => (
<Island priority="feature" defer={{ until: 'visible' }}>
<NewsletterSignupForm
newsletterId={identityName}
newsletterName={name}
frequency={frequency}
previewAction={previewAction}
componentId={componentId}
abTest={abTest}
isAlreadySubscribed={isSubscribed}
/>
</Island>
)}
<Island priority="feature" defer={{ until: 'visible' }}>
<NewsletterSignupForm
newsletterId={identityName}
newsletterName={name}
frequency={frequency}
componentId={componentId}
isAlreadySubscribed={isSubscribed}
/>
</Island>
</NewsletterSignupCardContainer>
</InlineSkipToWrapper>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const defaultArgs = {
frequency: 'Weekly',
theme: 'sport',
idApiUrl: 'https://idapi.theguardian.com',
exampleUrl: 'https://www.theguardian.com/email/the-recap',
illustrationSquare:
'https://i.guim.co.uk/img/uploads/2023/11/01/SaturdayEdition_-_5-3.jpg?width=220&dpr=2&s=none&crop=5%3A3',
} satisfies Story['args'];
Expand Down
Loading
Loading