feat(client): add settings side nav (#63034)

Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
Sem Bauke
2025-12-03 10:28:20 +01:00
committed by GitHub
parent ebf5a8463e
commit e056608d82
12 changed files with 546 additions and 135 deletions

View File

@@ -281,7 +281,7 @@
"disabled": "Your certifications will be disabled, if set to private.", "disabled": "Your certifications will be disabled, if set to private.",
"private-name": "Your name will not appear on your certifications, if this is set to private.", "private-name": "Your name will not appear on your certifications, if this is set to private.",
"claim-legacy": "Once you've earned the following freeCodeCamp certifications, you'll be able to claim the {{cert}}:", "claim-legacy": "Once you've earned the following freeCodeCamp certifications, you'll be able to claim the {{cert}}:",
"for": "Account Settings for {{username}}", "for": "Settings for {{username}}",
"sound-mode": "This adds the pleasant sound of acoustic guitar throughout the website. You'll get musical feedback as you type in the editor, complete challenges, claim certifications, and more.", "sound-mode": "This adds the pleasant sound of acoustic guitar throughout the website. You'll get musical feedback as you type in the editor, complete challenges, claim certifications, and more.",
"sound-volume": "Campfire Volume:", "sound-volume": "Campfire Volume:",
"scrollbar-width": "Editor Scrollbar Width", "scrollbar-width": "Editor Scrollbar Width",
@@ -328,12 +328,13 @@
"keyboard-shortcuts": "Enable Keyboard Shortcuts" "keyboard-shortcuts": "Enable Keyboard Shortcuts"
}, },
"headings": { "headings": {
"account": "Account",
"certs": "Certifications", "certs": "Certifications",
"legacy-certs": "Legacy Certifications", "legacy-certs": "Legacy Certifications",
"honesty": "Academic Honesty Policy", "honesty": "Academic Honesty Policy",
"internet": "Your Internet Presence", "internet": "Your Internet Presence",
"portfolio": "Portfolio Settings", "portfolio": "Portfolio Settings",
"privacy": "Privacy Settings", "privacy": "Privacy",
"personal-info": "Personal Information" "personal-info": "Personal Information"
}, },
"danger": { "danger": {
@@ -362,7 +363,7 @@
}, },
"email": { "email": {
"missing": "You do not have an email associated with this account.", "missing": "You do not have an email associated with this account.",
"heading": "Email Settings", "heading": "Email",
"not-verified": "Your email has not been verified.", "not-verified": "Your email has not been verified.",
"check": "Please check your email, or <0>request a new verification email here</0>.", "check": "Please check your email, or <0>request a new verification email here</0>.",
"current": "Current Email", "current": "Current Email",

View File

@@ -0,0 +1,71 @@
.settings-container {
display: flex;
min-height: calc(100vh - var(--header-height));
}
.settings-main {
flex: 4;
padding: 1rem;
}
.settings-sidebar-nav ul {
list-style-type: none;
padding-left: 0;
}
.settings-sidebar-nav ul li {
margin-bottom: 0.5rem;
}
.settings-sidebar-nav {
flex: 1;
position: sticky;
top: var(--header-height);
padding: 1rem 0;
overflow-y: scroll;
height: calc(100vh - var(--header-height));
border-right: 3px solid var(--tertiary-background);
}
.settings-sidebar-nav .sidebar-nav-section-heading {
display: block;
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
padding-left: 1rem;
text-decoration: none;
cursor: pointer;
}
.settings-sidebar-nav .sidebar-nav-anchor-btn {
display: inline-block;
width: 100%;
text-decoration: none;
cursor: pointer;
padding: 0 1rem 0rem 2rem;
}
.settings-sidebar-nav .sidebar-nav-section-heading.active,
.settings-sidebar-nav .sidebar-nav-anchor-btn.active {
box-shadow: inset 3px 0 0 0 var(--highlight-color);
background-color: var(--tertiary-background);
}
@media (max-width: 980px) {
.settings-sidebar-nav {
display: none;
}
.settings-container {
display: block;
}
}
.settings-user-token-heading,
.settings-danger-zone-heading,
.settings-exam-token-heading {
color: inherit;
font-size: inherit;
margin: 0;
text-align: center;
}

View File

@@ -3,22 +3,22 @@ import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { scroller } from 'react-scroll';
import { Container, Spacer } from '@freecodecamp/ui';
import { Spacer } from '@freecodecamp/ui';
import store from 'store'; import store from 'store';
import { scroller, Element as ScrollElement } from 'react-scroll';
import envData from '../../config/env.json'; import envData from '../../config/env.json';
import { createFlashMessage } from '../components/Flash/redux'; import { createFlashMessage } from '../components/Flash/redux';
import { Loader } from '../components/helpers'; import { Loader } from '../components/helpers';
import Certification from '../components/settings/certification'; import Certification from '../components/settings/certification';
import MiscSettings from '../components/settings/misc-settings'; import Account from '../components/settings/account';
import DangerZone from '../components/settings/danger-zone'; import DangerZone from '../components/settings/danger-zone';
import Email from '../components/settings/email'; import Email from '../components/settings/email';
import Honesty from '../components/settings/honesty'; import Honesty from '../components/settings/honesty';
import Privacy from '../components/settings/privacy'; import Privacy from '../components/settings/privacy';
import UserToken from '../components/settings/user-token'; import UserToken from '../components/settings/user-token';
import ExamToken from '../components/settings/exam-token'; import ExamToken from '../components/settings/exam-token';
import SettingsSidebarNav from '../components/settings/settings-sidebar-nav';
import { hardGoTo as navigate } from '../redux/actions'; import { hardGoTo as navigate } from '../redux/actions';
import { import {
signInLoadingSelector, signInLoadingSelector,
@@ -37,6 +37,8 @@ import {
resetMyEditorLayout resetMyEditorLayout
} from '../redux/settings/actions'; } from '../redux/settings/actions';
import './show-settings.css';
const { apiLocation } = envData; const { apiLocation } = envData;
// TODO: update types for actions // TODO: update types for actions
@@ -171,78 +173,99 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
return ( return (
<> <>
<Helmet title={`${t('buttons.settings')} | freeCodeCamp.org`} /> <Helmet title={`${t('buttons.settings')} | freeCodeCamp.org`} />
<Container> <div className='settings-container' id='settings-container'>
<main> <SettingsSidebarNav userToken={userToken} />
<main className='settings-main'>
<Spacer size='l' /> <Spacer size='l' />
<h1 <ScrollElement name='username'>
id='content-start' <h1
className='text-center' id='content-start'
style={{ overflowWrap: 'break-word' }} className='text-center'
data-playwright-test-label='settings-heading' style={{ overflowWrap: 'break-word' }}
> data-playwright-test-label='settings-heading'
{t('settings.for', { username: username })} >
</h1> {t('settings.for', { username: username })}
<MiscSettings </h1>
keyboardShortcuts={keyboardShortcuts} </ScrollElement>
sound={sound}
editorLayout={editorLayout}
resetEditorLayout={resetEditorLayout}
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
toggleSoundMode={toggleSoundMode}
/>
<Spacer size='m' /> <Spacer size='m' />
<Privacy /> <ScrollElement name='account'>
<Account
keyboardShortcuts={keyboardShortcuts}
sound={sound}
editorLayout={editorLayout}
resetEditorLayout={resetEditorLayout}
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
toggleSoundMode={toggleSoundMode}
/>
</ScrollElement>
<Spacer size='m' /> <Spacer size='m' />
<Email <ScrollElement name='privacy'>
email={email} <Privacy />
isEmailVerified={isEmailVerified} </ScrollElement>
sendQuincyEmail={sendQuincyEmail}
updateQuincyEmail={updateQuincyEmail}
/>
<Spacer size='m' /> <Spacer size='m' />
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} /> <ScrollElement name='email'>
<Email
email={email}
isEmailVerified={isEmailVerified}
sendQuincyEmail={sendQuincyEmail}
updateQuincyEmail={updateQuincyEmail}
/>
</ScrollElement>
<Spacer size='m' /> <Spacer size='m' />
<ExamToken email={email} /> <ScrollElement name='honesty'>
<Certification <Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
completedChallenges={completedChallenges} </ScrollElement>
createFlashMessage={createFlashMessage} <Spacer size='m' />
is2018DataVisCert={is2018DataVisCert} <ScrollElement name='exam-token'>
isA2EnglishCert={isA2EnglishCert} <ExamToken email={email} />
isApisMicroservicesCert={isApisMicroservicesCert} </ScrollElement>
isBackEndCert={isBackEndCert} <Spacer size='m' />
isDataAnalysisPyCertV7={isDataAnalysisPyCertV7} <ScrollElement name='certifications'>
isDataVisCert={isDataVisCert} <Certification
isCollegeAlgebraPyCertV8={isCollegeAlgebraPyCertV8} completedChallenges={completedChallenges}
isFoundationalCSharpCertV8={isFoundationalCSharpCertV8} createFlashMessage={createFlashMessage}
isFrontEndCert={isFrontEndCert} is2018DataVisCert={is2018DataVisCert}
isFrontEndLibsCert={isFrontEndLibsCert} isA2EnglishCert={isA2EnglishCert}
isFullStackCert={isFullStackCert} isApisMicroservicesCert={isApisMicroservicesCert}
isJavascriptCertV9={isJavascriptCertV9} isBackEndCert={isBackEndCert}
isHonest={isHonest} isDataAnalysisPyCertV7={isDataAnalysisPyCertV7}
isInfosecCertV7={isInfosecCertV7} isDataVisCert={isDataVisCert}
isInfosecQaCert={isInfosecQaCert} isCollegeAlgebraPyCertV8={isCollegeAlgebraPyCertV8}
isJsAlgoDataStructCert={isJsAlgoDataStructCert} isFoundationalCSharpCertV8={isFoundationalCSharpCertV8}
isMachineLearningPyCertV7={isMachineLearningPyCertV7} isFrontEndCert={isFrontEndCert}
isQaCertV7={isQaCertV7} isFrontEndLibsCert={isFrontEndLibsCert}
isRelationalDatabaseCertV8={isRelationalDatabaseCertV8} isFullStackCert={isFullStackCert}
isRespWebDesignCert={isRespWebDesignCert} isJavascriptCertV9={isJavascriptCertV9}
isRespWebDesignCertV9={isRespWebDesignCertV9} isHonest={isHonest}
isSciCompPyCertV7={isSciCompPyCertV7} isInfosecCertV7={isInfosecCertV7}
isJsAlgoDataStructCertV8={isJsAlgoDataStructCertV8} isInfosecQaCert={isInfosecQaCert}
username={username} isJsAlgoDataStructCert={isJsAlgoDataStructCert}
verifyCert={verifyCert} isMachineLearningPyCertV7={isMachineLearningPyCertV7}
isEmailVerified={isEmailVerified} isQaCertV7={isQaCertV7}
/> isRelationalDatabaseCertV8={isRelationalDatabaseCertV8}
isRespWebDesignCert={isRespWebDesignCert}
isRespWebDesignCertV9={isRespWebDesignCertV9}
isSciCompPyCertV7={isSciCompPyCertV7}
isJsAlgoDataStructCertV8={isJsAlgoDataStructCertV8}
username={username}
verifyCert={verifyCert}
isEmailVerified={isEmailVerified}
/>
<Spacer size='m' />
</ScrollElement>
{userToken && ( {userToken && (
<> <>
<ScrollElement name='user-token'>
<UserToken />
</ScrollElement>
<Spacer size='m' /> <Spacer size='m' />
<UserToken />
</> </>
)} )}
<Spacer size='m' /> <ScrollElement name='danger-zone'>
<DangerZone /> <DangerZone />
</ScrollElement>
</main> </main>
</Container> </div>
</> </>
); );
} }

View File

@@ -3,9 +3,10 @@ import { useTranslation } from 'react-i18next';
import { Button, Spacer } from '@freecodecamp/ui'; import { Button, Spacer } from '@freecodecamp/ui';
import { FullWidthRow } from '../helpers'; import { FullWidthRow } from '../helpers';
import SoundSettings from '../../components/settings/sound'; import SoundSettings from './sound';
import KeyboardShortcutsSettings from '../../components/settings/keyboard-shortcuts'; import KeyboardShortcutsSettings from './keyboard-shortcuts';
import ScrollbarWidthSettings from '../../components/settings/scrollbar-width'; import ScrollbarWidthSettings from './scrollbar-width';
import SectionHeader from './section-header';
type MiscSettingsProps = { type MiscSettingsProps = {
keyboardShortcuts: boolean; keyboardShortcuts: boolean;
@@ -27,8 +28,8 @@ const MiscSettings = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <div className='account-settings'>
<Spacer size='m' /> <SectionHeader>{t('settings.headings.account')}</SectionHeader>
<FullWidthRow> <FullWidthRow>
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} /> <SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
<KeyboardShortcutsSettings <KeyboardShortcutsSettings
@@ -51,7 +52,7 @@ const MiscSettings = ({
{t('settings.reset-editor-layout')} {t('settings.reset-editor-layout')}
</Button> </Button>
</FullWidthRow> </FullWidthRow>
</> </div>
); );
}; };

View File

@@ -155,60 +155,66 @@ const LegacyFullStack = (props: CertificationSettingsProps) => {
}; };
return ( return (
<FullWidthRow key={certSlug}> <Element name={`cert-${certSlug}`}>
<Spacer size='m' /> <FullWidthRow key={certSlug}>
<h3 className='text-center'> <Spacer size='m' />
{t(`certification.title.${Certification.LegacyFullStack}-cert`)} <h3 className='text-center'>
</h3> {t(`certification.title.${Certification.LegacyFullStack}`)}
<div> </h3>
<p> <div>
{t('settings.claim-legacy', { <p>
cert: t(`certification.title.${Certification.LegacyFullStack}-cert`) {t('settings.claim-legacy', {
})} cert: t(
</p> `certification.title.${Certification.LegacyFullStack}-cert`
<ul> )
<li>{t(`certification.title.${Certification.RespWebDesign}`)}</li> })}
<li>{t(`certification.title.${Certification.JsAlgoDataStruct}`)}</li> </p>
<li>{t(`certification.title.${Certification.FrontEndDevLibs}`)}</li> <ul>
<li>{t(`certification.title.${Certification.DataVis}`)}</li> <li>{t(`certification.title.${Certification.RespWebDesign}`)}</li>
<li>{t(`certification.title.${Certification.BackEndDevApis}`)}</li> <li>
<li>{t(`certification.title.${Certification.LegacyInfoSecQa}`)}</li> {t(`certification.title.${Certification.JsAlgoDataStruct}`)}
</ul> </li>
</div> <li>{t(`certification.title.${Certification.FrontEndDevLibs}`)}</li>
<li>{t(`certification.title.${Certification.DataVis}`)}</li>
<li>{t(`certification.title.${Certification.BackEndDevApis}`)}</li>
<li>{t(`certification.title.${Certification.LegacyInfoSecQa}`)}</li>
</ul>
</div>
<div> <div>
{isFullStackCert ? ( {isFullStackCert ? (
<Button <Button
size='small' size='small'
variant='primary' variant='primary'
block={true} block={true}
href={certLocation} href={certLocation}
id={'button-' + certSlug} id={'button-' + certSlug}
target='_blank' target='_blank'
> >
{t('buttons.show-cert')}{' '} {t('buttons.show-cert')}{' '}
<span className='sr-only'> <span className='sr-only'>
{t(`certification.title.${Certification.LegacyFullStack}`)} {t(`certification.title.${Certification.LegacyFullStack}`)}
</span> </span>
</Button> </Button>
) : ( ) : (
<Button <Button
size='small' size='small'
variant='primary' variant='primary'
block={true} block={true}
disabled={!fullStackClaimable} disabled={!fullStackClaimable}
id={'button-' + certSlug} id={'button-' + certSlug}
onClick={handleClaim(certSlug)} onClick={handleClaim(certSlug)}
> >
{t('buttons.claim-cert')}{' '} {t('buttons.claim-cert')}{' '}
<span className='sr-only'> <span className='sr-only'>
{t(`certification.title.${Certification.LegacyFullStack}`)} {t(`certification.title.${Certification.LegacyFullStack}`)}
</span> </span>
</Button> </Button>
)} )}
</div> </div>
<Spacer size='m' /> <Spacer size='m' />
</FullWidthRow> </FullWidthRow>
</Element>
); );
}; };
@@ -385,7 +391,9 @@ function CertificationSettings(props: CertificationSettingsProps) {
<Certification key={cert} certSlug={cert} t={t} /> <Certification key={cert} certSlug={cert} t={t} />
))} ))}
<Spacer size='m' /> <Spacer size='m' />
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader> <Element name='legacy-certifications'>
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
</Element>
<LegacyFullStack {...props} /> <LegacyFullStack {...props} />
{legacyCertifications.map(cert => ( {legacyCertifications.map(cert => (
<Certification key={cert} certSlug={cert} t={t} /> <Certification key={cert} certSlug={cert} t={t} />

View File

@@ -43,7 +43,11 @@ function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
return ( return (
<FullWidthRow className='text-center'> <FullWidthRow className='text-center'>
<Panel variant='danger' id='danger-zone'> <Panel variant='danger' id='danger-zone'>
<Panel.Heading>{t('settings.danger.heading')}</Panel.Heading> <Panel.Heading>
<h2 className='settings-danger-zone-heading'>
{t('settings.danger.heading')}
</h2>
</Panel.Heading>
<Spacer size='m' /> <Spacer size='m' />
<p>{t('settings.danger.be-careful')}</p> <p>{t('settings.danger.be-careful')}</p>
<FullWidthRow> <FullWidthRow>

View File

@@ -95,7 +95,11 @@ function ExamToken({ email }: ExamTokenProps) {
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
<Panel variant='info' id='exam-environment-authorization-token'> <Panel variant='info' id='exam-environment-authorization-token'>
<Panel.Heading>{t('exam-token.exam-token')}</Panel.Heading> <Panel.Heading>
<h2 className='settings-exam-token-heading'>
{t('exam-token.exam-token')}
</h2>
</Panel.Heading>
<Panel.Body> <Panel.Body>
<p>{t('exam-token.note')}</p> <p>{t('exam-token.note')}</p>
<strong>{t('exam-token.invalidation-2')}</strong> <strong>{t('exam-token.invalidation-2')}</strong>

View File

@@ -0,0 +1,232 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link as ScrollLink } from 'react-scroll';
import {
currentCertifications,
legacyCertifications,
legacyFullStackCertification,
upcomingCertifications
} from '../../../../shared-dist/config/certification-settings';
import env from '../../../config/env.json';
type SettingsSidebarNavProps = {
userToken: string | null;
};
const { showUpcomingChanges } = env;
function SettingsSidebarNav({
userToken
}: SettingsSidebarNavProps): JSX.Element {
const { t } = useTranslation();
const allLegacyCertifications = [
...legacyFullStackCertification,
...legacyCertifications
];
return (
<aside className='settings-sidebar-nav'>
<ul>
<li>
<ScrollLink
to='account'
href='#account'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('settings.headings.account')}
</ScrollLink>
</li>
<li>
<ScrollLink
to='privacy'
href='#privacy'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('settings.headings.privacy')}
</ScrollLink>
</li>
<li>
<ScrollLink
to='email'
href='#email'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('settings.email.heading')}
</ScrollLink>
</li>
<li>
<ScrollLink
to='honesty'
href='#honesty'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('settings.headings.honesty')}
</ScrollLink>
</li>
<li>
<ScrollLink
to='exam-token'
href='#exam-token'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('exam-token.exam-token')}
</ScrollLink>
</li>
<li>
<ScrollLink
to='certifications'
href='#certifications'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('settings.headings.certs')}
</ScrollLink>
<ul>
{currentCertifications.map(slug => (
<li key={slug}>
<ScrollLink
to={`cert-${slug}`}
href={`#cert-${slug}`}
className={'sidebar-nav-anchor-btn'}
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t(`certification.title.${slug}`, slug)}
</ScrollLink>
</li>
))}
</ul>
</li>
<li>
<ScrollLink
to='legacy-certifications'
href='#legacy-certifications'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('settings.headings.legacy-certs')}
</ScrollLink>
<ul>
{allLegacyCertifications.map(slug => (
<li key={slug}>
<ScrollLink
to={`cert-${slug}`}
href={`#cert-${slug}`}
className={'sidebar-nav-anchor-btn'}
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t(`certification.title.${slug}`, slug)}
</ScrollLink>
</li>
))}
</ul>
<ul>
{showUpcomingChanges &&
upcomingCertifications.map(slug => (
<li key={slug}>
<ScrollLink
to={`cert-${slug}`}
href={`#cert-${slug}`}
className={'sidebar-nav-anchor-btn'}
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t(`certification.title.${slug}`, slug)}
</ScrollLink>
</li>
))}
</ul>
</li>
{userToken && (
<li>
<ScrollLink
to='user-token'
href='#user-token'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('user-token.title')}
</ScrollLink>
</li>
)}
<li>
<ScrollLink
to='danger-zone'
href='#danger-zone'
className='sidebar-nav-section-heading'
smooth={true}
offset={-48}
duration={300}
spy={true}
hashSpy={true}
activeClass='active'
>
{t('settings.danger.heading')}
</ScrollLink>
</li>
</ul>
</aside>
);
}
SettingsSidebarNav.displayName = 'SettingsSidebarNav';
export default SettingsSidebarNav;

View File

@@ -29,7 +29,11 @@ class UserToken extends Component<UserTokenProps> {
return ( return (
<FullWidthRow> <FullWidthRow>
<Panel variant='info' className='text-center'> <Panel variant='info' className='text-center'>
<Panel.Heading>{t('user-token.title')}</Panel.Heading> <Panel.Heading>
<h2 className='settings-user-token-heading'>
{t('user-token.title')}
</h2>
</Panel.Heading>
<Spacer size='m' /> <Spacer size='m' />
<Panel.Body> <Panel.Body>
<p>{t('user-token.delete-p1')}</p> <p>{t('user-token.delete-p1')}</p>

View File

@@ -0,0 +1,61 @@
import { test, expect } from '@playwright/test';
test.use({ storageState: 'playwright/.auth/certified-user.json' });
test.beforeEach(async ({ page }) => {
// Set viewport to desktop size to ensure sideNav is visible
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('/settings');
// Wait for the main heading to appear
await expect(
page.getByRole('heading', { level: 1, name: 'Settings for certifieduser' })
).toBeVisible();
});
test.describe('Settings SideNav Component', () => {
test('should display the settings sideNav with links to all main sections', async ({
page,
isMobile
}) => {
test.setTimeout(30000);
test.skip(isMobile, 'Sidebar is hidden on mobile');
const sideNav = page.getByRole('complementary');
const main = page.getByRole('main');
// Get all h2 and h3 heading in the main section
const h2Texts = await main
.getByRole('heading', { level: 2 })
.allTextContents();
const h3Texts = await main
.getByRole('heading', { level: 3 })
.allTextContents();
const headingTexts = [...h2Texts, ...h3Texts];
// Make sure the sideNav contains the same number of links as headings
const sideNavLinks = sideNav.getByRole('link');
await expect(sideNavLinks).toHaveCount(headingTexts.length);
// For each heading text, find the link by accessible name and test click behavior
for (const headingText of headingTexts) {
const link = sideNav.getByRole('link', {
name: headingText,
exact: true
});
await expect(link).toBeVisible();
// Get the href and assert the URL ends with it after click
const href = await link.getAttribute('href');
await link.click();
// Wait for scroll animation
// Playwright performs click very fast, which could lead to URL check before scroll ends
await page.waitForTimeout(300);
await expect(page).toHaveURL(new RegExp(href + '$'));
}
});
});

View File

@@ -232,7 +232,7 @@ test.describe('Settings - Certified User', () => {
} }
// Danger Zone // Danger Zone
await expect(page.getByText('Danger Zone')).toBeVisible(); await expect(page.getByRole('main').getByText('Danger Zone')).toBeVisible();
await expect( await expect(
page.getByText( page.getByText(
'Please be careful. Changes in this section are permanent.' 'Please be careful. Changes in this section are permanent.'

View File

@@ -18,7 +18,7 @@ test.describe('Initially', () => {
test('should not render', async ({ page }) => { test('should not render', async ({ page }) => {
await page.goto('/settings'); await page.goto('/settings');
await expect( await expect(
page.getByText('User Token', { exact: true }) page.getByRole('main').getByText('User Token', { exact: true })
).not.toBeVisible(); ).not.toBeVisible();
}); });
}); });
@@ -32,7 +32,9 @@ test.describe('After creating token', () => {
await page.goto('/settings'); await page.goto('/settings');
// Set `exact` to `true` to only match the panel heading // Set `exact` to `true` to only match the panel heading
await expect(page.getByText('User Token', { exact: true })).toBeVisible(); await expect(
page.getByRole('main').getByText('User Token', { exact: true })
).toBeVisible();
await expect( await expect(
page.getByText( page.getByText(
'Your user token is used to save your progress on curriculum sections that use a virtual machine. If you suspect it has been compromised, you can delete it without losing any progress. A new one will be created automatically the next time you open a project.' 'Your user token is used to save your progress on curriculum sections that use a virtual machine. If you suspect it has been compromised, you can delete it without losing any progress. A new one will be created automatically the next time you open a project.'
@@ -42,7 +44,7 @@ test.describe('After creating token', () => {
await alertToBeVisible(page, translations.flash['token-deleted']); await alertToBeVisible(page, translations.flash['token-deleted']);
await expect( await expect(
page.getByText('User Token', { exact: true }) page.getByRole('main').getByText('User Token', { exact: true })
).not.toBeVisible(); ).not.toBeVisible();
}); });
}); });