mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat/ab test landing google auth (#62538)
This commit is contained in:
@@ -42,6 +42,37 @@ describe('auth0 plugin', () => {
|
||||
await fastify.register(prismaPlugin);
|
||||
});
|
||||
|
||||
describe('GET /signin/google', () => {
|
||||
test('should redirect directly to Google via Auth0 with connection param', async () => {
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/signin/google'
|
||||
});
|
||||
const redirectUrl = new URL(res.headers.location!);
|
||||
expect(redirectUrl.host).toMatch(AUTH0_DOMAIN);
|
||||
expect(redirectUrl.pathname).toBe('/authorize');
|
||||
expect(redirectUrl.searchParams.get('connection')).toBe('google-oauth2');
|
||||
expect(res.statusCode).toBe(302);
|
||||
});
|
||||
|
||||
test('sets a login-returnto cookie', async () => {
|
||||
const returnTo = 'http://localhost:3000/learn';
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/signin/google',
|
||||
headers: { referer: returnTo }
|
||||
});
|
||||
const cookie = res.cookies.find(c => c.name === 'login-returnto');
|
||||
expect(unsign(cookie!.value).value).toBe(returnTo);
|
||||
expect(cookie).toMatchObject({
|
||||
domain: COOKIE_DOMAIN,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Lax'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fastify.prisma.$runCommandRaw({ dropDatabase: 1 });
|
||||
await fastify.close();
|
||||
|
||||
@@ -82,6 +82,25 @@ export const auth0Client: FastifyPluginCallbackTypebox = fp(
|
||||
);
|
||||
void reply.redirect(redirectUrl);
|
||||
});
|
||||
|
||||
fastify.get('/signin/google', async function (request, reply) {
|
||||
const returnTo = request.headers.referer ?? `${HOME_LOCATION}/learn`;
|
||||
void reply.setCookie('login-returnto', returnTo, {
|
||||
domain: COOKIE_DOMAIN,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
signed: true,
|
||||
sameSite: 'lax'
|
||||
});
|
||||
|
||||
const authorizationEndpoint =
|
||||
await this.auth0OAuth.generateAuthorizationUri(request, reply);
|
||||
|
||||
const url = new URL(authorizationEndpoint);
|
||||
url.searchParams.set('connection', 'google-oauth2');
|
||||
|
||||
void reply.redirect(url.toString());
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
@@ -110,13 +110,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"show-benefits": {
|
||||
"landing-two-button-cta": {
|
||||
"defaultValue": false,
|
||||
"rules": [
|
||||
{
|
||||
"coverage": 1,
|
||||
"hashAttribute": "id",
|
||||
"seed": "show-benefits",
|
||||
"seed": "landing-two-button-cta",
|
||||
"hashVersion": 2,
|
||||
"variations": [
|
||||
false,
|
||||
@@ -126,7 +126,7 @@
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"key": "show-benefits",
|
||||
"key": "landing-two-button-cta",
|
||||
"meta": [
|
||||
{
|
||||
"key": "0",
|
||||
@@ -138,7 +138,7 @@
|
||||
}
|
||||
],
|
||||
"phase": "0",
|
||||
"name": "prod-show-benefits"
|
||||
"name": "prod-landing-two-button-cta"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@
|
||||
"share-on-threads": "Share on Threads",
|
||||
"play-scene": "Press Play",
|
||||
"download-latest-version": "Download the Latest Version",
|
||||
"more-ways-to-sign-in": "More ways to sign in",
|
||||
"sign-in-with-google": "Sign in with Google",
|
||||
"go-to-dcc-today": "Go to Today's Challenge",
|
||||
"go-to-dcc-archive": "Go to Daily Coding Challenge Archive"
|
||||
},
|
||||
@@ -893,7 +895,8 @@
|
||||
"fsd-b-benefit-2-title": "Professional Certification",
|
||||
"fsd-b-benefit-2-description": "Prove your skills with an official, verifiable certification.",
|
||||
"fsd-b-benefit-3-title": "500+ Exercises",
|
||||
"fsd-b-benefit-3-description": "Solidify your knowledge with plenty of practice."
|
||||
"fsd-b-benefit-3-description": "Solidify your knowledge with plenty of practice.",
|
||||
"or": "OR"
|
||||
},
|
||||
"icons": {
|
||||
"gold-cup": "Gold Cup",
|
||||
|
||||
@@ -29,13 +29,14 @@ const Login = ({
|
||||
isSignedIn
|
||||
}: LoginProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const href = isSignedIn ? `${homeLocation}/learn` : `${apiLocation}/signin`;
|
||||
return (
|
||||
<a
|
||||
className={(block ? 'btn-cta-big btn-block' : '') + ' signup-btn btn-cta'}
|
||||
data-test-label={dataTestLabel}
|
||||
data-playwright-test-label='header-sign-in-button'
|
||||
data-playwright-test-label={
|
||||
dataTestLabel ? dataTestLabel : 'sign-in-button'
|
||||
}
|
||||
href={href}
|
||||
onClick={() => {
|
||||
callGA({
|
||||
@@ -43,9 +44,8 @@ const Login = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className='login-btn-icon'>
|
||||
<span className='login-btn-icon' aria-hidden='true'>
|
||||
<FontAwesomeIcon icon={faRightToBracket} />
|
||||
<span className='sr-only'> {t('buttons.sign-in')}</span>
|
||||
</span>
|
||||
<span className='login-btn-text'>{children || t('buttons.sign-in')}</span>
|
||||
</a>
|
||||
|
||||
@@ -2,14 +2,20 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Login from '../../Header/components/login';
|
||||
|
||||
const BigCallToAction = ({ text }: { text?: string }): JSX.Element => {
|
||||
const BigCallToAction = ({
|
||||
text,
|
||||
testLabel
|
||||
}: {
|
||||
text?: string;
|
||||
testLabel?: string;
|
||||
}): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Login
|
||||
block={true}
|
||||
data-test-label='landing-big-cta'
|
||||
data-playwright-test-label='landing-big-cta'
|
||||
data-test-label={testLabel}
|
||||
data-playwright-test-label={testLabel}
|
||||
>
|
||||
{text ? text : t('buttons.logged-in-cta-btn')}
|
||||
</Login>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Container, Col, Row, Spacer } from '@freecodecamp/ui';
|
||||
import { clientLocale } from '../../../../config/env.json';
|
||||
import {
|
||||
AmazonLogo,
|
||||
AppleLogo,
|
||||
MicrosoftLogo,
|
||||
SpotifyLogo,
|
||||
GoogleLogo,
|
||||
TencentLogo,
|
||||
AlibabaLogo
|
||||
} from '../../../assets/images/components';
|
||||
import BigCallToAction from './big-call-to-action';
|
||||
import CampersImage from './campers-image';
|
||||
|
||||
function LandingTop(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const showChineseLogos = ['chinese', 'chinese-tradition'].includes(
|
||||
clientLocale
|
||||
);
|
||||
return (
|
||||
<Container
|
||||
fluid={true}
|
||||
className='landing-top lading-top-c gradient-container'
|
||||
>
|
||||
<Container>
|
||||
<Row className='landing-top-two-column'>
|
||||
<Spacer size='m' />
|
||||
<Col className='landing-top-left'>
|
||||
<h1
|
||||
id='content-start'
|
||||
className='ultra-heading'
|
||||
data-test-label='landing-header'
|
||||
data-playwright-test-label='big-heading-1-b'
|
||||
>
|
||||
{t('landing.big-heading-1-b')}
|
||||
</h1>
|
||||
<p data-playwright-test-label='advance-career'>
|
||||
{t('landing.advance-career')}
|
||||
</p>
|
||||
<Spacer size='m' />
|
||||
|
||||
<BigCallToAction />
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
<Col className='landing-top-right'>
|
||||
<CampersImage />
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12} className='brands-container'>
|
||||
<Spacer size='l' />
|
||||
<p data-playwright-test-label='graduates-work'>
|
||||
<Trans>landing.graduates-work</Trans>
|
||||
</p>
|
||||
<Spacer size='s' />
|
||||
<div
|
||||
className='logo-row'
|
||||
data-playwright-test-label='brand-logo-container'
|
||||
>
|
||||
<AppleLogo />
|
||||
<GoogleLogo />
|
||||
<MicrosoftLogo />
|
||||
{showChineseLogos ? (
|
||||
<>
|
||||
<TencentLogo />
|
||||
<AlibabaLogo />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SpotifyLogo />
|
||||
<AmazonLogo />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
LandingTop.displayName = 'LandingTop';
|
||||
export default LandingTop;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useFeature } from '@growthbook/growthbook-react';
|
||||
import { Container, Col, Row, Spacer } from '@freecodecamp/ui';
|
||||
import { clientLocale } from '../../../../config/env.json';
|
||||
import {
|
||||
@@ -12,78 +13,78 @@ import {
|
||||
AlibabaLogo
|
||||
} from '../../../assets/images/components';
|
||||
import BigCallToAction from './big-call-to-action';
|
||||
import TwoButtonCTA from './two-button-cta';
|
||||
import CampersImage from './campers-image';
|
||||
|
||||
const LogoRow = (): JSX.Element => {
|
||||
function LandingTop(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const showTwoButtonCTA = useFeature('landing-two-button-cta').on;
|
||||
const showChineseLogos = ['chinese', 'chinese-tradition'].includes(
|
||||
clientLocale
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='logo-row-title' data-playwright-test-label='h2-heading'>
|
||||
<Trans>landing.h2-heading</Trans>
|
||||
</p>
|
||||
<div
|
||||
className='logo-row'
|
||||
data-playwright-test-label='brand-logo-container'
|
||||
>
|
||||
<AppleLogo />
|
||||
<GoogleLogo />
|
||||
<MicrosoftLogo />
|
||||
{showChineseLogos ? (
|
||||
<>
|
||||
<TencentLogo />
|
||||
<AlibabaLogo />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SpotifyLogo />
|
||||
<AmazonLogo />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function LandingTop(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Container fluid={true} className='gradient-container'>
|
||||
<Container className='landing-top'>
|
||||
<Spacer size='m' />
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Container
|
||||
fluid={true}
|
||||
className='landing-top lading-top-c gradient-container'
|
||||
>
|
||||
<Container>
|
||||
<Row className='landing-top-two-column' data-test-label='landing-top'>
|
||||
<Spacer size='m' />
|
||||
<Col className='landing-top-left'>
|
||||
<h1
|
||||
id='content-start'
|
||||
className='mega-heading'
|
||||
className='ultra-heading'
|
||||
data-test-label='landing-header'
|
||||
data-playwright-test-label='landing-big-heading-1'
|
||||
data-playwright-test-label='big-heading-1-b'
|
||||
>
|
||||
{t('landing.big-heading-1')}
|
||||
{t('landing.big-heading-1-b')}
|
||||
</h1>
|
||||
<p
|
||||
className='mega-heading'
|
||||
data-playwright-test-label='landing-big-heading-2'
|
||||
>
|
||||
{t('landing.big-heading-2')}
|
||||
<p data-playwright-test-label='advance-career'>
|
||||
{t('landing.advance-career')}
|
||||
</p>
|
||||
<p
|
||||
className='mega-heading'
|
||||
data-playwright-test-label='landing-big-heading-3'
|
||||
>
|
||||
{t('landing.big-heading-3')}
|
||||
</p>
|
||||
<LogoRow />
|
||||
<Spacer size='m' />
|
||||
<BigCallToAction />
|
||||
|
||||
{showTwoButtonCTA ? (
|
||||
<TwoButtonCTA />
|
||||
) : (
|
||||
<BigCallToAction testLabel='landing-top-big-cta' />
|
||||
)}
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
<Col className='landing-top-right'>
|
||||
<CampersImage />
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<CampersImage />
|
||||
<Col sm={10} smOffset={1} xs={12} className='brands-container'>
|
||||
<Spacer size='l' />
|
||||
<p data-playwright-test-label='graduates-work'>
|
||||
<Trans>landing.graduates-work</Trans>
|
||||
</p>
|
||||
<Spacer size='s' />
|
||||
<div
|
||||
className='logo-row'
|
||||
data-playwright-test-label='brand-logo-container'
|
||||
>
|
||||
<AppleLogo />
|
||||
<GoogleLogo />
|
||||
<MicrosoftLogo />
|
||||
{showChineseLogos ? (
|
||||
<>
|
||||
<TencentLogo />
|
||||
<AlibabaLogo />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SpotifyLogo />
|
||||
<AmazonLogo />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { createStore } from '../../../redux/create-store';
|
||||
import TwoButtonCTA from './two-button-cta';
|
||||
|
||||
vi.mock('../../../utils/get-words');
|
||||
|
||||
const renderWithStore = ({ isSignedIn }: { isSignedIn: boolean }) => {
|
||||
const preloadedState = {
|
||||
app: {
|
||||
user: {
|
||||
sessionUser: isSignedIn ? { id: 'user-id', username: 'fcc-user' } : null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const store = createStore(preloadedState);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<TwoButtonCTA />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const googleCta = screen.getByRole('link', {
|
||||
name: 'buttons.sign-in-with-google'
|
||||
});
|
||||
const moreWaysCta = screen.getByRole('link', {
|
||||
name: 'buttons.more-ways-to-sign-in'
|
||||
});
|
||||
|
||||
return { googleCta, moreWaysCta };
|
||||
};
|
||||
|
||||
describe('Stacked landing CTA', () => {
|
||||
test('renders Google and More ways CTAs', () => {
|
||||
const { googleCta, moreWaysCta } = renderWithStore({ isSignedIn: false });
|
||||
|
||||
expect(googleCta).toBeInTheDocument();
|
||||
expect(moreWaysCta).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('links go to API when signed out', () => {
|
||||
const { googleCta, moreWaysCta } = renderWithStore({ isSignedIn: false });
|
||||
|
||||
expect(googleCta).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringMatching(/\/signin\/google$/)
|
||||
);
|
||||
expect(moreWaysCta).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringMatching(/\/signin$/)
|
||||
);
|
||||
});
|
||||
|
||||
test('links go to learn when signed in', () => {
|
||||
const { googleCta, moreWaysCta } = renderWithStore({ isSignedIn: true });
|
||||
|
||||
expect(googleCta).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringMatching(/\/learn$/)
|
||||
);
|
||||
expect(moreWaysCta).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringMatching(/\/learn$/)
|
||||
);
|
||||
});
|
||||
});
|
||||
98
client/src/components/landing/components/two-button-cta.tsx
Normal file
98
client/src/components/landing/components/two-button-cta.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import envData from '../../../../config/env.json';
|
||||
import { isSignedInSelector } from '../../../redux/selectors';
|
||||
import callGA from '../../../analytics/call-ga';
|
||||
|
||||
const { apiLocation, homeLocation } = envData as {
|
||||
apiLocation: string;
|
||||
homeLocation: string;
|
||||
};
|
||||
|
||||
interface TwoButtonCTAProps {
|
||||
isSignedIn?: boolean;
|
||||
}
|
||||
|
||||
const TwoButtonCTA = ({ isSignedIn }: TwoButtonCTAProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleGoogleClick = () => callGA({ event: 'sign_in' });
|
||||
const handleMoreWaysClick = () => callGA({ event: 'sign_in' });
|
||||
|
||||
const googleHref = isSignedIn
|
||||
? `${homeLocation}/learn`
|
||||
: `${apiLocation}/signin/google`;
|
||||
const moreWaysHref = isSignedIn
|
||||
? `${homeLocation}/learn`
|
||||
: `${apiLocation}/signin`;
|
||||
|
||||
return (
|
||||
<div className='two-button-cta'>
|
||||
<a
|
||||
className='signup-btn btn btn-sm btn-block google-btn'
|
||||
data-test-label='landing-google-cta'
|
||||
data-playwright-test-label='landing-google-cta'
|
||||
href={googleHref}
|
||||
onClick={handleGoogleClick}
|
||||
>
|
||||
<span className='google-btn-icon' aria-hidden='true'>
|
||||
{/* Google "G" logo box per brand guidelines */}
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 48 48'
|
||||
width='18'
|
||||
height='18'
|
||||
>
|
||||
<path
|
||||
fill='#EA4335'
|
||||
d='M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.91-6.91C35.9 2.3 30.37 0 24 0 14.62 0 6.51 5.38 2.56 13.22l8.9 6.91C13.42 14.83 18.27 9.5 24 9.5z'
|
||||
/>
|
||||
<path
|
||||
fill='#4285F4'
|
||||
d='M46.5 24c0-1.64-.15-3.2-.44-4.71H24v8.92h12.65c-.55 2.96-2.2 5.47-4.71 7.17l7.23 5.61C43.9 36.78 46.5 30.86 46.5 24z'
|
||||
/>
|
||||
<path
|
||||
fill='#FBBC05'
|
||||
d='M11.46 28.13A14.5 14.5 0 0 1 10.5 24c0-1.46.25-2.87.7-4.19l-8.9-6.91A23.93 23.93 0 0 0 0 24c0 3.91.94 7.6 2.6 10.86l8.86-6.73z'
|
||||
/>
|
||||
<path
|
||||
fill='#34A853'
|
||||
d='M24 48c6.48 0 11.93-2.14 15.9-5.86l-7.23-5.61c-2 1.35-4.62 2.13-8.67 2.13-5.73 0-10.58-5.33-12.53-10.63l-8.9 6.91C6.51 42.62 14.62 48 24 48z'
|
||||
/>
|
||||
<path fill='none' d='M0 0h48v48H0z' />
|
||||
</svg>
|
||||
</span>
|
||||
<span>{t('buttons.sign-in-with-google')}</span>
|
||||
</a>
|
||||
|
||||
<div className='cta-divider' role='separator' aria-label='or'>
|
||||
<span className='cta-divider-line' />
|
||||
<span>{t('misc.or')}</span>
|
||||
<span className='cta-divider-line' />
|
||||
</div>
|
||||
|
||||
<a
|
||||
className='signup-btn btn btn-sm btn-block more-ways-btn'
|
||||
data-test-label='landing-more-ways-cta'
|
||||
data-playwright-test-label='landing-more-ways-cta'
|
||||
href={moreWaysHref}
|
||||
onClick={handleMoreWaysClick}
|
||||
>
|
||||
<span className='login-btn-text'>
|
||||
{t('buttons.more-ways-to-sign-in')}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TwoButtonCTA.displayName = 'TwoButtonCTA';
|
||||
|
||||
const mapStateToProps = createSelector(isSignedInSelector, isSignedIn => ({
|
||||
isSignedIn
|
||||
}));
|
||||
|
||||
export default connect(mapStateToProps)(TwoButtonCTA);
|
||||
@@ -217,7 +217,6 @@ figcaption.caption {
|
||||
}
|
||||
}
|
||||
|
||||
/* AB testing styles */
|
||||
.landing-top .mega-heading {
|
||||
font-size: 2.2rem;
|
||||
margin: 0px 0px 2rem;
|
||||
@@ -230,6 +229,109 @@ figcaption.caption {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.two-button-cta {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.two-button-cta {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.google-btn {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 3px solid #ffffff;
|
||||
color: #000;
|
||||
display: inline-flex;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-size: 1rem;
|
||||
min-height: 48px;
|
||||
padding: 10px 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.google-btn:hover,
|
||||
.google-btn:focus {
|
||||
background: #000;
|
||||
border-color: #ffffff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.light-palette .google-btn {
|
||||
background: #000;
|
||||
border-color: #000000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.light-palette .google-btn:hover,
|
||||
.light-palette .google-btn:focus {
|
||||
background: #ffffff;
|
||||
border-color: #000000;
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.google-btn svg {
|
||||
display: block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.google-btn-icon {
|
||||
border-radius: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
padding-inline-end: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.more-ways-btn {
|
||||
align-items: center;
|
||||
background-image: linear-gradient(#fecc4c, #ffac33);
|
||||
border: 3px solid #feac32;
|
||||
color: #0a0a23;
|
||||
display: inline-flex;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
padding: 12px 20px;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.more-ways-btn:hover,
|
||||
.more-ways-btn:focus {
|
||||
background-color: #fecc4c;
|
||||
background-image: none;
|
||||
border-color: #f1a02a;
|
||||
color: #0a0a23;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cta-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--secondary-color);
|
||||
text-transform: lowercase;
|
||||
font-size: 0.95rem;
|
||||
margin-block: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cta-divider-line {
|
||||
height: 1px;
|
||||
background: none;
|
||||
border-top: 1px dashed var(--gray-45);
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.landing-top .mega-heading {
|
||||
font-size: 3rem;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useGrowthBook } from '@growthbook/growthbook-react';
|
||||
import SEO from '../components/seo';
|
||||
import { Loader } from '../components/helpers';
|
||||
import LandingTopB from '../components/landing/components/landing-top-b';
|
||||
import LandingTop from '../components/landing/components/landing-top';
|
||||
import Testimonials from '../components/landing/components/testimonials';
|
||||
import Certifications from '../components/landing/components/certifications';
|
||||
@@ -11,17 +10,13 @@ import Faq from '../components/landing/components/faq';
|
||||
import Benefits from '../components/landing/components/benefits';
|
||||
import '../components/landing/landing.css';
|
||||
|
||||
type LandingProps = {
|
||||
showLandingPageRedesign: boolean;
|
||||
};
|
||||
|
||||
const Landing = ({ showLandingPageRedesign }: LandingProps) => (
|
||||
const Landing = () => (
|
||||
<main
|
||||
id='landing-content'
|
||||
data-testid='landing-content'
|
||||
className={`landing-page`}
|
||||
>
|
||||
{showLandingPageRedesign ? <LandingTopB /> : <LandingTop />}
|
||||
<LandingTop />
|
||||
<Benefits />
|
||||
<Testimonials />
|
||||
<Certifications />
|
||||
@@ -33,20 +28,14 @@ function IndexPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const growthbook = useGrowthBook();
|
||||
if (growthbook && growthbook.ready) {
|
||||
console.error('GrowthBook Ready', growthbook);
|
||||
const showLandingPageRedesign = growthbook.getFeatureValue(
|
||||
'landing-top-skill-focused',
|
||||
false
|
||||
);
|
||||
growthbook.getFeatureValue('landing-aa-test', false);
|
||||
return (
|
||||
<>
|
||||
<SEO title={t('metaTags:title')} />
|
||||
<Landing showLandingPageRedesign={showLandingPageRedesign} />
|
||||
<Landing />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
console.error('GrowthBook not ready yet', growthbook);
|
||||
return (
|
||||
<>
|
||||
<SEO title={t('metaTags:title')} />
|
||||
|
||||
@@ -15,7 +15,7 @@ const headerComponentElements = {
|
||||
languageButton: 'header-lang-list-option',
|
||||
menuButton: 'header-menu-button',
|
||||
menu: 'header-menu',
|
||||
signInButton: 'header-sign-in-button'
|
||||
signInButton: 'sign-in-button'
|
||||
} as const;
|
||||
|
||||
const examUrl =
|
||||
|
||||
@@ -6,13 +6,15 @@ import { addGrowthbookCookie } from './utils/add-growthbook-cookie';
|
||||
|
||||
const landingPageElements = {
|
||||
heading: 'landing-header',
|
||||
callToAction: 'landing-big-cta',
|
||||
certifications: 'certifications',
|
||||
curriculumBtns: 'curriculum-map-button',
|
||||
testimonials: 'testimonial-card',
|
||||
landingPageImage: 'landing-page-figure',
|
||||
faq: 'landing-page-faq',
|
||||
jobs: 'More than <strong>100,000</strong> freeCodeCamp.org graduates have gotten <strong>jobs</strong> at tech companies including:'
|
||||
jobs: 'More than <strong>100,000</strong> freeCodeCamp.org graduates have gotten <strong>jobs</strong> at tech companies including:',
|
||||
googleCTA: 'landing-google-cta',
|
||||
moreWaysCTA: 'landing-more-ways-cta',
|
||||
landingTopCta: 'landing-top-big-cta'
|
||||
} as const;
|
||||
|
||||
const nonArchivedSuperBlocks = [
|
||||
@@ -30,11 +32,68 @@ async function goToLandingPage(page: Page) {
|
||||
await page.goto('/');
|
||||
}
|
||||
|
||||
test.describe('Landing Top - Variation B', () => {
|
||||
test.describe('Main CTA - Variation A', () => {
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await addGrowthbookCookie({ context, variation: 'A' });
|
||||
await goToLandingPage(page);
|
||||
});
|
||||
test('Five main CTAs render correctly', async ({ page }) => {
|
||||
const landingTopCta = page.getByTestId(landingPageElements.landingTopCta);
|
||||
const googleCTA = page.getByTestId(landingPageElements.googleCTA);
|
||||
const moreWaysCTA = page.getByTestId(landingPageElements.moreWaysCTA);
|
||||
const ctas = page.getByRole('link', {
|
||||
name: translations.buttons['logged-in-cta-btn']
|
||||
});
|
||||
const benefitsCtas = page.getByRole('link', {
|
||||
name: translations.landing.benefits.cta
|
||||
});
|
||||
await expect(benefitsCtas).toHaveCount(1);
|
||||
await expect(landingTopCta).toHaveText(
|
||||
translations.buttons['logged-in-cta-btn']
|
||||
);
|
||||
await expect(ctas).toHaveCount(4);
|
||||
for (const cta of await ctas.all()) {
|
||||
await expect(cta).toBeVisible();
|
||||
}
|
||||
await expect(googleCTA).toBeHidden();
|
||||
await expect(moreWaysCTA).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Main CTA - Variation B', () => {
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await addGrowthbookCookie({ context, variation: 'B' });
|
||||
await goToLandingPage(page);
|
||||
});
|
||||
test('Four main and two stacked CTAs render correctly', async ({ page }) => {
|
||||
const landingTopCta = page.getByTestId(landingPageElements.landingTopCta);
|
||||
const googleCTA = page.getByTestId(landingPageElements.googleCTA);
|
||||
const moreWaysCTA = page.getByTestId(landingPageElements.moreWaysCTA);
|
||||
const ctas = page.getByRole('link', {
|
||||
name: translations.buttons['logged-in-cta-btn']
|
||||
});
|
||||
const benefitsCtas = page.getByRole('link', {
|
||||
name: translations.landing.benefits.cta
|
||||
});
|
||||
await expect(benefitsCtas).toHaveCount(1);
|
||||
await expect(landingTopCta).toBeHidden();
|
||||
await expect(ctas).toHaveCount(3);
|
||||
for (const cta of await ctas.all()) {
|
||||
await expect(cta).toBeVisible();
|
||||
}
|
||||
await expect(googleCTA).toHaveText(
|
||||
translations.buttons['sign-in-with-google']
|
||||
);
|
||||
await expect(moreWaysCTA).toHaveText(
|
||||
translations.buttons['more-ways-to-sign-in']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Landing Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await goToLandingPage(page);
|
||||
});
|
||||
|
||||
test('Main heading copy renders correctly', async ({ page }) => {
|
||||
const bigHeading = page.getByTestId('big-heading-1-b');
|
||||
@@ -54,61 +113,6 @@ test.describe('Landing Top - Variation B', () => {
|
||||
translations.landing['graduates-work'].replace(/<\/?strong>/g, '')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
*
|
||||
* not currently in use after https://github.com/freeCodeCamp/freeCodeCamp/pull/61359
|
||||
* bring back after we fix GB
|
||||
*/
|
||||
|
||||
// test.describe('Landing Top - Variation A', () => {
|
||||
// test.beforeEach(async ({ context, page }) => {
|
||||
// await addGrowthbookCookie({ context, variation: 'newA' });
|
||||
// await goToLandingPage(page);
|
||||
// });
|
||||
|
||||
// test('The headline renders correctly', async ({ page }) => {
|
||||
// const landingHeading1 = page.getByTestId('landing-big-heading-1');
|
||||
// await expect(landingHeading1).toHaveText(
|
||||
// translations.landing['big-heading-1']
|
||||
// );
|
||||
|
||||
// const landingHeading2 = page.getByTestId('landing-big-heading-2');
|
||||
// await expect(landingHeading2).toHaveText(
|
||||
// translations.landing['big-heading-2']
|
||||
// );
|
||||
|
||||
// const landingHeading3 = page.getByTestId('landing-big-heading-3');
|
||||
// await expect(landingHeading3).toHaveText(
|
||||
// translations.landing['big-heading-3']
|
||||
// );
|
||||
// });
|
||||
|
||||
// test('Logo row copy renders correctly', async ({ page }) => {
|
||||
// const landingH2Heading = page.getByTestId('h2-heading');
|
||||
// await expect(landingH2Heading).toHaveText(
|
||||
// translations.landing['h2-heading'].replace(/<\/?strong>/g, '')
|
||||
// );
|
||||
// });
|
||||
|
||||
// test('Hero image should have a description', async ({ isMobile, page }) => {
|
||||
// const captionText = page.getByText(
|
||||
// translations.landing['hero-img-description']
|
||||
// );
|
||||
|
||||
// if (isMobile) {
|
||||
// await expect(captionText).toBeHidden();
|
||||
// } else {
|
||||
// await expect(captionText).toBeVisible();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
test.describe('Landing Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await goToLandingPage(page);
|
||||
});
|
||||
|
||||
test('The component Why learn with freeCodeCamp renders correctly', async ({
|
||||
context,
|
||||
@@ -123,16 +127,6 @@ test.describe('Landing Page', () => {
|
||||
await expect(h2Element).toBeVisible();
|
||||
});
|
||||
|
||||
test('Call to action buttons should render correctly', async ({ page }) => {
|
||||
const ctas = page.getByRole('link', {
|
||||
name: translations.buttons['logged-in-cta-btn']
|
||||
});
|
||||
await expect(ctas).toHaveCount(4);
|
||||
for (const cta of await ctas.all()) {
|
||||
await expect(cta).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('Hero image should have an alt', async ({ isMobile, page }) => {
|
||||
const campersImage = page.getByAltText(
|
||||
translations.landing['hero-img-alt']
|
||||
|
||||
Reference in New Issue
Block a user