Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions dotcom-rendering/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ sb.mock(import('../src/lib/fetchEmail.ts'), { spy: true });
sb.mock(import('../src/lib/useNewsletterSignupForm.ts'), { spy: true });
// @ts-ignore -- Storybook wants the file extension, TS does not.
sb.mock(import('../src/lib/useAB.ts'), { spy: true });
// @ts-ignore -- Storybook wants the file extension, TS does not.
sb.mock(import('../src/lib/useCountryCode.ts'), { spy: true });

// Prevent components being lazy rendered when we're taking Chromatic snapshots
Lazy.disabled = isChromatic();
Expand Down
245 changes: 245 additions & 0 deletions dotcom-rendering/src/components/ManyNewsletterSignUp.island.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { render, screen, waitFor } from '@testing-library/react';
import user from '@testing-library/user-event';
import {
reportTrackingEvent,
requestMultipleSignUps,
} from '../lib/newsletter-sign-up-requests';
import { useAuthStatus, useIsSignedIn } from '../lib/useAuthStatus';
import { useCountryCode } from '../lib/useCountryCode';
import { ConfigProvider } from './ConfigContext';
import { ManyNewsletterSignUp } from './ManyNewsletterSignUp.island';
import { BUTTON_ROLE } from './NewsletterCard';

const NEWSLETTER_IDENTITY_NAME = 'morning-briefing';
const NEWSLETTER_LIST_ID = 1234;
const TEST_EMAIL = 'reader@example.com';
const SIGN_UP_BUTTON_NAME = 'Sign up for the newsletter you selected';

jest.mock('../lib/useAuthStatus', () => ({
useAuthStatus: jest.fn().mockReturnValue({ kind: 'SignedOut' }),
useIsSignedIn: jest.fn(),
}));

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

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

jest.mock('../lib/newsletter-sign-up-requests', () => ({
reportTrackingEvent: jest.fn().mockResolvedValue(undefined),
requestMultipleSignUps: jest.fn(),
}));

jest.mock('react-google-recaptcha', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => null),
}));

const renderSignupForm = () =>
render(
<ConfigProvider
value={{
renderingTarget: 'Web',
darkModeAvailable: false,
assetOrigin: '/',
editionId: 'UK',
}}
>
<ManyNewsletterSignUp useReCaptcha={false} />
</ConfigProvider>,
);

/**
* Adds a fake newsletter toggle button to the document body so that
* ManyNewsletterSignUp can discover it via its DOM event listeners.
* Must be added before renderSignupForm() so the useEffect attaches listeners.
*/
const addNewsletterButton = (
identityName: string,
listId: number,
): HTMLButtonElement => {
const button = document.createElement('button');
button.setAttribute('data-role', BUTTON_ROLE);
button.setAttribute('data-identity-name', identityName);
button.setAttribute('data-list-id', String(listId));
document.body.appendChild(button);
return button;
};

const openSignupForm = async (
testUser: ReturnType<typeof user.setup>,
): Promise<void> => {
const newsletterButton = addNewsletterButton(
NEWSLETTER_IDENTITY_NAME,
NEWSLETTER_LIST_ID,
);
renderSignupForm();
await testUser.click(newsletterButton);

await waitFor(() => {
expect(screen.getByLabelText('Enter your email')).toBeInTheDocument();
});
};

const submitForm = async (
testUser: ReturnType<typeof user.setup>,
email = TEST_EMAIL,
): Promise<void> => {
await testUser.type(screen.getByLabelText('Enter your email'), email);
await testUser.click(
screen.getByRole('button', { name: SIGN_UP_BUTTON_NAME }),
);
};

describe('ManyNewsletterSignUp', () => {
const pageConfig = window.guardian.config
.page as typeof window.guardian.config.page & {
ajaxUrl?: string;
};

beforeEach(() => {
jest.clearAllMocks();

(useIsSignedIn as jest.Mock).mockReturnValue(false);
(useAuthStatus as jest.Mock).mockReturnValue({ kind: 'SignedOut' });
(useCountryCode as jest.Mock).mockReturnValue(undefined);

pageConfig.ajaxUrl = 'https://api.nextgen.guardianapps.co.uk';
window.guardian.config.switches['usSignupHideMarketingToggle'] = false;

(requestMultipleSignUps as jest.Mock).mockResolvedValue({ ok: true });
});

afterEach(() => {
for (const el of document.querySelectorAll(
`[data-role=${BUTTON_ROLE}]`,
)) {
el.remove();
}
});

describe('US hide marketing toggle (usSignupHideMarketingToggle switch)', () => {
beforeEach(() => {
window.guardian.config.switches['usSignupHideMarketingToggle'] =
true;
});

it('switch on + US + signed out: hides marketing checkbox, sends marketingOptInHidden=true', async () => {
const testUser = user.setup();
(useCountryCode as jest.Mock).mockReturnValue('US');
await openSignupForm(testUser);

expect(
screen.queryByLabelText(/Get updates about our journalism/),
).not.toBeInTheDocument();

await submitForm(testUser);

await waitFor(() => {
expect(requestMultipleSignUps).toHaveBeenCalledWith(
TEST_EMAIL,
[NEWSLETTER_IDENTITY_NAME],
'',
true,
true,
);
});
});

it('switch on + US + signed out: tracking uses similar-guardian-products-optin-hidden-us', async () => {
const testUser = user.setup();
(useCountryCode as jest.Mock).mockReturnValue('US');
await openSignupForm(testUser);
await submitForm(testUser);

await waitFor(() => {
expect(reportTrackingEvent).toHaveBeenCalledWith(
'ManyNewsletterSignUp',
'success-response',
expect.anything(),
expect.objectContaining({
marketingOptInType:
'similar-guardian-products-optin-hidden-us',
}),
);
});
});

it('switch on + non-US + signed out: shows checkbox, no marketingOptInHidden', async () => {
const testUser = user.setup();
(useCountryCode as jest.Mock).mockReturnValue('GB');
await openSignupForm(testUser);

expect(
await screen.findByLabelText(
/Get updates about our journalism/,
),
).toBeInTheDocument();

await submitForm(testUser);

await waitFor(() => {
expect(requestMultipleSignUps).toHaveBeenCalledWith(
TEST_EMAIL,
[NEWSLETTER_IDENTITY_NAME],
'',
true,
undefined,
);
});
});

it('switch on + pending country (undefined) + signed out: shows checkbox', async () => {
const testUser = user.setup();
(useCountryCode as jest.Mock).mockReturnValue(undefined);
await openSignupForm(testUser);

expect(
await screen.findByLabelText(
/Get updates about our journalism/,
),
).toBeInTheDocument();
});

it('switch on + US + signed in: checkbox not shown (signed-in), no marketingOptInHidden', async () => {
const testUser = user.setup();
(useCountryCode as jest.Mock).mockReturnValue('US');
(useIsSignedIn as jest.Mock).mockReturnValue(true);
(useAuthStatus as jest.Mock).mockReturnValue({ kind: 'SignedIn' });
await openSignupForm(testUser);

expect(
screen.queryByLabelText(/Get updates about our journalism/),
).not.toBeInTheDocument();

await submitForm(testUser);

await waitFor(() => {
expect(requestMultipleSignUps).toHaveBeenCalledWith(
TEST_EMAIL,
[NEWSLETTER_IDENTITY_NAME],
'',
undefined,
undefined,
);
});
});

it('switch off + US + signed out: shows checkbox', async () => {
window.guardian.config.switches['usSignupHideMarketingToggle'] =
false;
const testUser = user.setup();
(useCountryCode as jest.Mock).mockReturnValue('US');
await openSignupForm(testUser);

expect(
await screen.findByLabelText(
/Get updates about our journalism/,
),
).toBeInTheDocument();
});
});
});
44 changes: 31 additions & 13 deletions dotcom-rendering/src/components/ManyNewsletterSignUp.island.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ import { useCallback, useEffect, useRef, useState } from 'react';
// that version will compile and render but is non-functional.
// Use the default export instead.
import type ReactGoogleRecaptcha from 'react-google-recaptcha';
import {
getEffectiveMarketingOptIn,
getMarketingOptInType,
} from '../lib/newsletter-marketing-opt-in';
import {
reportTrackingEvent,
requestMultipleSignUps,
} from '../lib/newsletter-sign-up-requests';
import { clearSubscriptionCache } from '../lib/newsletterSubscriptionCache';
import { useAuthStatus, useIsSignedIn } from '../lib/useAuthStatus';
import { useHideMarketingToggleForCountry } from '../lib/useHideMarketingToggleForCountry';
import { useConfig } from './ConfigContext';
import { Flex } from './Flex';
import { ManyNewslettersForm } from './ManyNewslettersForm';
Expand Down Expand Up @@ -137,6 +142,10 @@ export const ManyNewsletterSignUp = ({
}: Props) => {
const isSignedIn = useIsSignedIn();
const authStatus = useAuthStatus();
const hideMarketingToggle = useHideMarketingToggleForCountry();
/** True when the marketing toggle is hidden for this user due to country policy. */
const marketingOptInHiddenForCountry =
hideMarketingToggle && isSignedIn === false;

const [newslettersToSignUpFor, setNewslettersToSignUpFor] = useState<
Array<{
Expand Down Expand Up @@ -248,6 +257,16 @@ export const ManyNewsletterSignUp = ({
const listIds = newslettersToSignUpFor.map(
(newsletter) => newsletter.listId,
);
const effectiveMarketingOptIn = getEffectiveMarketingOptIn({
marketingOptInHiddenForCountry,
isSignedIn,
marketingOptIn,
});
const marketingOptInType = getMarketingOptInType({
marketingOptInHiddenForCountry,
isSignedIn,
effectiveMarketingOptIn,
});

void reportTrackingEvent(
'ManyNewsletterSignUp',
Expand All @@ -262,15 +281,12 @@ export const ManyNewsletterSignUp = ({
email,
identityNames,
reCaptchaToken,
marketingOptIn,
effectiveMarketingOptIn,
marketingOptInHiddenForCountry ? true : undefined,
).catch(() => {
return undefined;
});

const marketingOptInType = marketingOptIn
? 'similar-guardian-products-optin'
: 'similar-guardian-products-optout';

if (!response?.ok) {
const responseText = response
? await response.text()
Expand All @@ -281,12 +297,9 @@ export const ManyNewsletterSignUp = ({
renderingTarget,
{
listIds,
...(marketingOptIn !== undefined && { marketingOptInType }),
// If the backend handles the failure and responds with an informative
// error message (E.G. "Service unavailable", "Invalid email" etc) this
// should be included in the event data.
// If not, the response text will be the HTML for the default error page
// which would not be helpful to include it in the tracking data.
...(marketingOptInType !== undefined && {
marketingOptInType,
}),
responseText: responseText.substring(0, 100),
},
);
Expand All @@ -300,7 +313,7 @@ export const ManyNewsletterSignUp = ({
renderingTarget,
{
listIds,
...(marketingOptIn !== undefined && { marketingOptInType }),
...(marketingOptInType !== undefined && { marketingOptInType }),
},
);

Expand Down Expand Up @@ -418,7 +431,12 @@ export const ManyNewsletterSignUp = ({
status,
}}
newsletterCount={newslettersToSignUpFor.length}
marketingOptIn={marketingOptIn}
marketingOptIn={
marketingOptInHiddenForCountry ||
isSignedIn === true
? undefined
: marketingOptIn
}
setMarketingOptIn={setMarketingOptIn}
useReCaptcha={useReCaptcha}
captchaSiteKey={captchaSiteKey}
Expand Down
Loading
Loading