From b34291fc41f4e2cf4d6767cd607e8829aeca754a Mon Sep 17 00:00:00 2001 From: Ahmad Abdolsaheb Date: Fri, 16 Aug 2024 12:33:48 +0300 Subject: [PATCH] feat: add AB test landing page top (#55659) Co-authored-by: Naomi the Technomancer Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams --- .../config/growthbook-features-default.json | 35 + client/i18n/locales/english/translations.json | 5 + .../assets/images/landing/landing-page-b.svg | 987 ++++++++++++++++++ client/src/components/Donation/donation.css | 24 + .../growth-book/growth-book-wrapper.tsx | 26 +- .../landing/components/landing-top-b.tsx | 110 ++ .../landing/components/ui-images.tsx | 27 + client/src/components/landing/index.tsx | 56 +- client/src/components/landing/landing.css | 51 + e2e/landing.spec.ts | 358 ++++--- 10 files changed, 1525 insertions(+), 154 deletions(-) create mode 100644 client/config/growthbook-features-default.json create mode 100644 client/src/assets/images/landing/landing-page-b.svg create mode 100644 client/src/components/landing/components/landing-top-b.tsx create mode 100644 client/src/components/landing/components/ui-images.tsx diff --git a/client/config/growthbook-features-default.json b/client/config/growthbook-features-default.json new file mode 100644 index 00000000000..2be8bb28fb1 --- /dev/null +++ b/client/config/growthbook-features-default.json @@ -0,0 +1,35 @@ +{ + "aa-test": { + "defaultValue": false + }, + "aa-test-in-component": { + "defaultValue": false + }, + "landing-page-redesign": { + "defaultValue": false, + "rules": [ + { + "coverage": 1, + "hashAttribute": "id", + "seed": "landing-page-redesign", + "hashVersion": 2, + "variations": [false, true], + "weights": [0.5, 0.5], + "key": "landing-page-redesign", + "meta": [ + { + "key": "0", + "name": "Control" + }, + { + "key": "1", + "name": "Variation 1" + } + ], + "phase": "0", + "name": "tests the conversion rate of the new design comparing to the old one" + } + ] + } +} + diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index e4568923ca9..2529db851b0 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -1,6 +1,7 @@ { "buttons": { "logged-in-cta-btn": "Get started (it's free)", + "get-started": "Get Started", "logged-out-cta-btn": "Sign in to save your progress (it's free)", "view-curriculum": "View the Curriculum", "first-lesson": "Go to the first lesson", @@ -106,11 +107,15 @@ }, "landing": { "big-heading-1": "Learn to code — for free.", + "big-heading-1-b": "Learn to code.", "big-heading-2": "Build projects.", "big-heading-3": "Earn certifications.", + "big-heading-4": "All for free.", "h2-heading": "Since 2014, more than 40,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:", + "h2-heading-b": "More than 100,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:", "hero-img-description": "freeCodeCamp students at a local study group in South Korea.", "hero-img-alt": "A group of people, including a White man, a Black woman, and an Asian woman, gathered around a laptop.", + "hero-img-uis": "A group of screenshots showing the freeCodeCamp editor interface on both a mobile and desktop device and a certification.", "as-seen-in": "As seen in:", "testimonials": { "heading": "Here is what our alumni say about freeCodeCamp:", diff --git a/client/src/assets/images/landing/landing-page-b.svg b/client/src/assets/images/landing/landing-page-b.svg new file mode 100644 index 00000000000..eb4ed8aa8cb --- /dev/null +++ b/client/src/assets/images/landing/landing-page-b.svg @@ -0,0 +1,987 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/src/components/Donation/donation.css b/client/src/components/Donation/donation.css index bd01429de79..9136552a111 100644 --- a/client/src/components/Donation/donation.css +++ b/client/src/components/Donation/donation.css @@ -699,6 +699,30 @@ a.patreon-button:hover { ); } +.light-palette .gradient-foreground { + background-image: linear-gradient( + -10deg, + rgb(0, 51, 133) 35%, + rgba(237, 202, 216, 0) 75%, + rgb(150, 15, 46) 100% + ), + radial-gradient(circle, rgb(157, 1, 69) 0%, rgb(0, 89, 189) 100%); + color: transparent; + background-clip: text; +} + +.dark-palette .gradient-foreground { + background-image: linear-gradient( + -10deg, + rgb(223 243 255) 35%, + rgba(237, 202, 216, 0) 75%, + rgb(255 215 224) 100% + ), + radial-gradient(circle, rgb(255 139 189) 0%, rgb(187 219 255) 100%); + color: transparent; + background-clip: text; +} + .supporters-background { background-repeat: repeat; } diff --git a/client/src/components/growth-book/growth-book-wrapper.tsx b/client/src/components/growth-book/growth-book-wrapper.tsx index ccfd715a4cc..033dac76c2d 100644 --- a/client/src/components/growth-book/growth-book-wrapper.tsx +++ b/client/src/components/growth-book/growth-book-wrapper.tsx @@ -12,6 +12,7 @@ import { userFetchStateSelector } from '../../redux/selectors'; import envData from '../../../config/env.json'; +import defaultGrowthBookFeatures from '../../../config/growthbook-features-default.json'; import { User, UserFetchState } from '../../redux/prop-types'; import { getUUID } from '../../utils/growthbook-cookie'; import callGA from '../../analytics/call-ga'; @@ -77,17 +78,20 @@ const GrowthBookWrapper = ({ useEffect(() => { async function setGrowthBookFeatures() { - if (!growthbookUri) return; - - try { - const res = await fetch(growthbookUri); - const data = (await res.json()) as { - features: Record; - }; - growthbook.setFeatures(data.features); - } catch (e) { - // TODO: report to sentry when it's enabled - console.error(e); + if (!growthbookUri) { + // Defaults are added to facilitate testing, and avoid passing the related env + growthbook.setFeatures(defaultGrowthBookFeatures); + } else { + try { + const res = await fetch(growthbookUri); + const data = (await res.json()) as { + features: Record; + }; + growthbook.setFeatures(data.features); + } catch (e) { + // TODO: report to sentry when it's enabled + console.error(e); + } } } diff --git a/client/src/components/landing/components/landing-top-b.tsx b/client/src/components/landing/components/landing-top-b.tsx new file mode 100644 index 00000000000..eba471ceef4 --- /dev/null +++ b/client/src/components/landing/components/landing-top-b.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Container, Col, Row } from '@freecodecamp/ui'; +import { clientLocale } from '../../../../config/env.json'; +import { + AmazonLogo, + AppleLogo, + MicrosoftLogo, + SpotifyLogo, + GoogleLogo, + TencentLogo, + AlibabaLogo +} from '../../../assets/images/components'; +import { Spacer } from '../../helpers'; +import BigCallToAction from './big-call-to-action'; +import UIImages from './ui-images'; + +const LogoRow = (): JSX.Element => { + const { t } = useTranslation(); + const showChineseLogos = ['chinese', 'chinese-tradition'].includes( + clientLocale + ); + + return ( + <> +

+ {t('landing.h2-heading-b')} +

+ +
    + + + + {showChineseLogos ? ( + <> + + + + ) : ( + <> + + + + )} +
+ + ); +}; + +function LandingTop(): JSX.Element { + const { t } = useTranslation(); + + return ( + + + + + +

+ {t('landing.big-heading-1-b')} +

+

+ {t('landing.big-heading-2')} +

+

+ {t('landing.big-heading-3')} +

+

+ {t('landing.big-heading-4')} +

+ + + + + + + +
+ + + + + + +
+
+ ); +} + +LandingTop.displayName = 'LandingTop'; +export default LandingTop; diff --git a/client/src/components/landing/components/ui-images.tsx b/client/src/components/landing/components/ui-images.tsx new file mode 100644 index 00000000000..a483c8263b2 --- /dev/null +++ b/client/src/components/landing/components/ui-images.tsx @@ -0,0 +1,27 @@ +// eslint-disable-next-line filenames-simple/naming-convention +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Media from 'react-responsive'; +import landingPageb from '../../../assets/images/landing/landing-page-b.svg'; +import { LazyImage, Spacer } from '../../helpers'; + +const LARGE_SCREEN_SIZE = 1200; + +function UIImages(): JSX.Element { + const { t } = useTranslation(); + + return ( + +
+ +
+ +
+ ); +} + +UIImages.displayName = 'UIImages'; +export default UIImages; diff --git a/client/src/components/landing/index.tsx b/client/src/components/landing/index.tsx index e0cc6ca54c8..573809ff4d5 100644 --- a/client/src/components/landing/index.tsx +++ b/client/src/components/landing/index.tsx @@ -1,30 +1,58 @@ import React, { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; - +import { useGrowthBook } from '@growthbook/growthbook-react'; import SEO from '../seo'; +import { Loader } from '../helpers'; import AsSeenIn from './components/as-seen-in'; import Certifications from './components/certifications'; import LandingTop from './components/landing-top'; +import LandingTopB from './components/landing-top-b'; import Testimonials from './components/testimonials'; import Faq from './components/faq'; import './landing.css'; +const LandingA = () => ( +
+ + + + + +
+); + +const LandingB = () => ( +
+ + + + +
+); + function Landing(): ReactElement { const { t } = useTranslation(); - - return ( - <> - -
- - - - - -
- - ); + const growthbook = useGrowthBook(); + if (growthbook && growthbook.ready) { + const showLandingPageRedesign = growthbook.getFeatureValue( + 'landing-page-redesign', + false + ); + return ( + <> + + {showLandingPageRedesign === true ? : } + + ); + } else { + return ( + <> + + + + ); + } } Landing.displayName = 'Landing'; diff --git a/client/src/components/landing/landing.css b/client/src/components/landing/landing.css index 25cc6ed34af..724a0ba06a2 100644 --- a/client/src/components/landing/landing.css +++ b/client/src/components/landing/landing.css @@ -214,3 +214,54 @@ figcaption.caption { padding: 40px; } } + +/* AB testing styles */ +.landing-page-b .mega-heading { + font-size: 2rem; + margin: 0px 0px 1rem; + font-weight: 700; + line-height: 3rem; +} + +@media (min-width: 500px) { + .landing-page-b .mega-heading { + font-size: 3rem; + } +} + +.landing-page-b .landing-top .btn-cta-big { + margin: 0px; + min-width: 300px; + max-width: fit-content; + background-image: none; + color: var(--primary-background) !important; + background-color: var(--primary-color); + border-color: var(--primary-color); + padding: 12px; +} + +.landing-page-b .landing-top .btn-cta-big:hover, +.landing-page-b .landing-top .btn-cta-big:focus { + background-color: var(--quaternary-color) !important; +} + +.landing-page-b .logo-row-title { + font-weight: normal; +} + +.landing-page-b { + overflow-x: hidden; +} + +figure.ui-images { + position: absolute; + left: 50%; + height: auto; + width: 750px; + top: 3vw; +} + +figure.ui-images img { + width: 100%; + height: auto; +} diff --git a/e2e/landing.spec.ts b/e2e/landing.spec.ts index 9b265d99dc5..ec93697a00f 100644 --- a/e2e/landing.spec.ts +++ b/e2e/landing.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page } from '@playwright/test'; +import { BrowserContext, expect, Page, test } from '@playwright/test'; import intro from '../client/i18n/locales/english/intro.json'; import translations from '../client/i18n/locales/english/translations.json'; import { SuperBlocks } from '../shared/config/curriculum'; @@ -37,136 +37,236 @@ const superBlocks = [ intro[SuperBlocks.PythonForEverybody].title ]; -let page: Page; +async function addDefaultCookies(context: BrowserContext, variation: string) { + await context.addCookies([ + { + name: 'gbuuid', + value: variation, + domain: 'localhost', + path: '/', + expires: Math.floor(Date.now() / 1000) + 400 * 24 * 60 * 60 // 400 days from now + } + ]); +} -test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); +async function goToLandingPage(page: Page) { await page.goto('/'); -}); +} -test('The component Landing-top renders correctly', async () => { - 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'] - ); - - const landingH2Heading = page.getByTestId('landing-h2-heading'); - await expect(landingH2Heading).toHaveText(translations.landing['h2-heading']); -}); - -test('Has 5 brand logos', async () => { - const logos = page.getByTestId('brand-logo-container').locator('svg'); - await expect(logos).toHaveCount(5); - for (const logo of await logos.all()) { - await expect(logo).toBeVisible(); - } -}); - -test('The landing-top & testimonial sections should contain call-to-action, and have a descriptive text content', async () => { - const ctas = await page - .getByRole('link', { - name: translations.buttons['logged-in-cta-btn'] - }) - .all(); - - expect(ctas).toHaveLength(4); - - for (const cta of ctas) { - await expect(cta).toBeVisible(); - } -}); - -test("The landing-top should contain a descriptive text explaining the camper's image", async ({ - isMobile -}) => { - const campersImage = page.getByAltText(translations.landing['hero-img-alt']); - const captionText = page.getByText( - translations.landing['hero-img-description'] - ); - - if (isMobile) { - await expect(campersImage).toBeHidden(); - await expect(captionText).toBeHidden(); - } else { - await expect(campersImage).toBeVisible(); - await expect(captionText).toBeVisible(); - } -}); - -test('The campers landing page figure is visible on desktop and hidden on mobile view', async ({ - isMobile -}) => { - const landingPageImage = page.getByTestId('landing-page-figure'); - if (isMobile) { - await expect(landingPageImage).toBeHidden(); - } else { - await expect(landingPageImage).toBeVisible(); - } -}); - -test('The as seen in container is visible with featured logos', async () => { - const asSeenInContainer = page.getByTestId('landing-as-seen-in-text'); - await expect(asSeenInContainer).toHaveText( - translations.landing['as-seen-in'] - ); - const featuredLogos = page.getByTestId('landing-as-seen-in-container-logos'); - await expect(featuredLogos).toBeVisible(); -}); - -test('Testimonial section has a header', async () => { - const testimonialsHeader = page.getByTestId('testimonials-section-header'); - await expect(testimonialsHeader).toHaveText( - translations.landing.testimonials['heading'] - ); -}); - -test('Testimonial endorser people have images, occupation, location and testimony visible', async () => { - const cards = page.getByTestId('testimonial-card'); - await expect(cards).toHaveCount(3); - for (const card of await cards.all()) { - await expect(card).toBeVisible(); - await expect( - card.getByTestId('testimonials-endorser-image-container') - ).toBeVisible(); - await expect( - card.getByTestId('testimonials-endorser-location') - ).toBeVisible(); - await expect( - card.getByTestId('testimonials-endorser-occupation') - ).toBeVisible(); - await expect( - card.getByTestId('testimonials-endorser-testimony') - ).toBeVisible(); - } -}); - -test('Has links to all curriculum', async () => { - const curriculumBtns = page.getByTestId(landingPageElements.curriculumBtns); - await expect(curriculumBtns).toHaveCount(21); - for (let index = 0; index < superBlocks.length; index++) { - const btn = curriculumBtns.nth(index); - await expect(btn).toContainText(superBlocks[index]); - } -}); - -test('Has FAQ section', async () => { - const faqs = page.getByTestId(landingPageElements.faq); - await expect(faqs).toHaveCount(9); -}); - -test("Has CTA Get Started It's free buttons", async () => { - const ctaButtons = page.getByRole('link', { - name: "Get started (it's free)" +test.describe('Landing Page - Variation B', () => { + test.beforeEach(async ({ context, page }) => { + await addDefaultCookies(context, 'B'); + await goToLandingPage(page); + }); + + test('The component Landing-top renders correctly', async ({ page }) => { + await expect( + page + .getByRole('heading', { level: 1 }) + .filter({ hasText: `${translations.landing['big-heading-1-b']}` }) + ).toBeVisible(); + + 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'] + ); + + const landingHeading4 = page.getByTestId('landing-big-heading-4'); + await expect(landingHeading4).toHaveText( + translations.landing['big-heading-4'] + ); + + const landingH2Heading = page.getByTestId('landing-h2-heading-b'); + await expect(landingH2Heading).toHaveText( + translations.landing['h2-heading-b'] + ); + }); + + test('CTA buttons should render correctly', async ({ page }) => { + const mainCta = page.getByRole('link', { + name: translations.buttons['get-started'], + exact: true + }); + await expect(mainCta).toHaveCount(1); + for (const cta of await mainCta.all()) { + await expect(cta).toBeVisible(); + } + + const ctas = page.getByRole('link', { + name: translations.buttons['logged-in-cta-btn'], + exact: true + }); + await expect(ctas).toHaveCount(3); + for (const cta of await ctas.all()) { + await expect(cta).toBeVisible(); + } + }); + + test('Hero image should have a descriptive alt', async ({ + isMobile, + page + }) => { + const campersImage = page.getByAltText( + translations.landing['hero-img-uis'] + ); + + if (isMobile) { + await expect(campersImage).toBeHidden(); + } else { + await expect(campersImage).toBeVisible(); + } + }); + + test('The as seen in container with featured logos should not exist', async ({ + page + }) => { + const asSeenInContainer = page.getByTestId('landing-as-seen-in-text'); + await expect(asSeenInContainer).toHaveCount(0); + }); +}); + +test.describe('Landing Page - Variation A', () => { + test.beforeEach(async ({ context, page }) => { + await addDefaultCookies(context, 'A'); + await goToLandingPage(page); + }); + + test('The component Landing-top 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'] + ); + + const landingH2Heading = page.getByTestId('landing-h2-heading'); + await expect(landingH2Heading).toHaveText( + translations.landing['h2-heading'] + ); + }); + + 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 and a description', async ({ + isMobile, + page + }) => { + const campersImage = page.getByAltText( + translations.landing['hero-img-alt'] + ); + const captionText = page.getByText( + translations.landing['hero-img-description'] + ); + + if (isMobile) { + await expect(campersImage).toBeHidden(); + await expect(captionText).toBeHidden(); + } else { + await expect(campersImage).toBeVisible(); + await expect(captionText).toBeVisible(); + } + }); + + test('The as seen in container is visible with featured logos', async ({ + page + }) => { + const asSeenInContainer = page.getByTestId('landing-as-seen-in-text'); + await expect(asSeenInContainer).toHaveText( + translations.landing['as-seen-in'] + ); + const featuredLogos = page.getByTestId( + 'landing-as-seen-in-container-logos' + ); + await expect(featuredLogos).toBeVisible(); + }); +}); + +test.describe('Landing Page - common', () => { + test.beforeEach(async ({ page }) => { + await goToLandingPage(page); + }); + + test('Has 5 brand logos', async ({ page }) => { + const logos = page.getByTestId('brand-logo-container').locator('svg'); + await expect(logos).toHaveCount(5); + for (const logo of await logos.all()) { + await expect(logo).toBeVisible(); + } + }); + + test('The campers landing page figure is visible on desktop and hidden on mobile view', async ({ + page, + isMobile + }) => { + const landingPageImage = page.getByTestId('landing-page-figure'); + if (isMobile) { + await expect(landingPageImage).toBeHidden(); + } else { + await expect(landingPageImage).toBeVisible(); + } + }); + + test('Testimonial section has a header', async ({ page }) => { + const testimonialsHeader = page.getByTestId('testimonials-section-header'); + await expect(testimonialsHeader).toHaveText( + translations.landing.testimonials['heading'] + ); + }); + + test('Testimonial endorser people have images, occupation, location and testimony visible', async ({ + page + }) => { + const cards = page.getByTestId('testimonial-card'); + await expect(cards).toHaveCount(3); + for (const card of await cards.all()) { + await expect(card).toBeVisible(); + await expect( + card.getByTestId('testimonials-endorser-image-container') + ).toBeVisible(); + await expect( + card.getByTestId('testimonials-endorser-location') + ).toBeVisible(); + await expect( + card.getByTestId('testimonials-endorser-occupation') + ).toBeVisible(); + await expect( + card.getByTestId('testimonials-endorser-testimony') + ).toBeVisible(); + } + }); + + test('Has links to all curriculum', async ({ page }) => { + const curriculumBtns = page.getByTestId(landingPageElements.curriculumBtns); + await expect(curriculumBtns).toHaveCount(21); + for (let index = 0; index < superBlocks.length; index++) { + const btn = curriculumBtns.nth(index); + await expect(btn).toContainText(superBlocks[index]); + } + }); + + test('Has FAQ section', async ({ page }) => { + const faqs = page.getByTestId(landingPageElements.faq); + await expect(faqs).toHaveCount(9); }); - await expect(ctaButtons).toHaveCount(4); });