mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
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:
@@ -281,7 +281,7 @@
|
||||
"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.",
|
||||
"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-volume": "Campfire Volume:",
|
||||
"scrollbar-width": "Editor Scrollbar Width",
|
||||
@@ -328,12 +328,13 @@
|
||||
"keyboard-shortcuts": "Enable Keyboard Shortcuts"
|
||||
},
|
||||
"headings": {
|
||||
"account": "Account",
|
||||
"certs": "Certifications",
|
||||
"legacy-certs": "Legacy Certifications",
|
||||
"honesty": "Academic Honesty Policy",
|
||||
"internet": "Your Internet Presence",
|
||||
"portfolio": "Portfolio Settings",
|
||||
"privacy": "Privacy Settings",
|
||||
"privacy": "Privacy",
|
||||
"personal-info": "Personal Information"
|
||||
},
|
||||
"danger": {
|
||||
@@ -362,7 +363,7 @@
|
||||
},
|
||||
"email": {
|
||||
"missing": "You do not have an email associated with this account.",
|
||||
"heading": "Email Settings",
|
||||
"heading": "Email",
|
||||
"not-verified": "Your email has not been verified.",
|
||||
"check": "Please check your email, or <0>request a new verification email here</0>.",
|
||||
"current": "Current Email",
|
||||
|
||||
71
client/src/client-only-routes/show-settings.css
Normal file
71
client/src/client-only-routes/show-settings.css
Normal 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;
|
||||
}
|
||||
@@ -3,22 +3,22 @@ import Helmet from 'react-helmet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
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 { scroller, Element as ScrollElement } from 'react-scroll';
|
||||
import envData from '../../config/env.json';
|
||||
import { createFlashMessage } from '../components/Flash/redux';
|
||||
import { Loader } from '../components/helpers';
|
||||
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 Email from '../components/settings/email';
|
||||
import Honesty from '../components/settings/honesty';
|
||||
import Privacy from '../components/settings/privacy';
|
||||
import UserToken from '../components/settings/user-token';
|
||||
import ExamToken from '../components/settings/exam-token';
|
||||
import SettingsSidebarNav from '../components/settings/settings-sidebar-nav';
|
||||
import { hardGoTo as navigate } from '../redux/actions';
|
||||
import {
|
||||
signInLoadingSelector,
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
resetMyEditorLayout
|
||||
} from '../redux/settings/actions';
|
||||
|
||||
import './show-settings.css';
|
||||
|
||||
const { apiLocation } = envData;
|
||||
|
||||
// TODO: update types for actions
|
||||
@@ -171,9 +173,11 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Helmet title={`${t('buttons.settings')} | freeCodeCamp.org`} />
|
||||
<Container>
|
||||
<main>
|
||||
<div className='settings-container' id='settings-container'>
|
||||
<SettingsSidebarNav userToken={userToken} />
|
||||
<main className='settings-main'>
|
||||
<Spacer size='l' />
|
||||
<ScrollElement name='username'>
|
||||
<h1
|
||||
id='content-start'
|
||||
className='text-center'
|
||||
@@ -182,7 +186,10 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
>
|
||||
{t('settings.for', { username: username })}
|
||||
</h1>
|
||||
<MiscSettings
|
||||
</ScrollElement>
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='account'>
|
||||
<Account
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
sound={sound}
|
||||
editorLayout={editorLayout}
|
||||
@@ -190,19 +197,30 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||
toggleSoundMode={toggleSoundMode}
|
||||
/>
|
||||
</ScrollElement>
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='privacy'>
|
||||
<Privacy />
|
||||
</ScrollElement>
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='email'>
|
||||
<Email
|
||||
email={email}
|
||||
isEmailVerified={isEmailVerified}
|
||||
sendQuincyEmail={sendQuincyEmail}
|
||||
updateQuincyEmail={updateQuincyEmail}
|
||||
/>
|
||||
</ScrollElement>
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='honesty'>
|
||||
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
|
||||
</ScrollElement>
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='exam-token'>
|
||||
<ExamToken email={email} />
|
||||
</ScrollElement>
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='certifications'>
|
||||
<Certification
|
||||
completedChallenges={completedChallenges}
|
||||
createFlashMessage={createFlashMessage}
|
||||
@@ -233,16 +251,21 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
verifyCert={verifyCert}
|
||||
isEmailVerified={isEmailVerified}
|
||||
/>
|
||||
<Spacer size='m' />
|
||||
</ScrollElement>
|
||||
{userToken && (
|
||||
<>
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='user-token'>
|
||||
<UserToken />
|
||||
</ScrollElement>
|
||||
<Spacer size='m' />
|
||||
</>
|
||||
)}
|
||||
<Spacer size='m' />
|
||||
<ScrollElement name='danger-zone'>
|
||||
<DangerZone />
|
||||
</ScrollElement>
|
||||
</main>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Button, Spacer } from '@freecodecamp/ui';
|
||||
import { FullWidthRow } from '../helpers';
|
||||
|
||||
import SoundSettings from '../../components/settings/sound';
|
||||
import KeyboardShortcutsSettings from '../../components/settings/keyboard-shortcuts';
|
||||
import ScrollbarWidthSettings from '../../components/settings/scrollbar-width';
|
||||
import SoundSettings from './sound';
|
||||
import KeyboardShortcutsSettings from './keyboard-shortcuts';
|
||||
import ScrollbarWidthSettings from './scrollbar-width';
|
||||
import SectionHeader from './section-header';
|
||||
|
||||
type MiscSettingsProps = {
|
||||
keyboardShortcuts: boolean;
|
||||
@@ -27,8 +28,8 @@ const MiscSettings = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spacer size='m' />
|
||||
<div className='account-settings'>
|
||||
<SectionHeader>{t('settings.headings.account')}</SectionHeader>
|
||||
<FullWidthRow>
|
||||
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
||||
<KeyboardShortcutsSettings
|
||||
@@ -51,7 +52,7 @@ const MiscSettings = ({
|
||||
{t('settings.reset-editor-layout')}
|
||||
</Button>
|
||||
</FullWidthRow>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -155,20 +155,25 @@ const LegacyFullStack = (props: CertificationSettingsProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Element name={`cert-${certSlug}`}>
|
||||
<FullWidthRow key={certSlug}>
|
||||
<Spacer size='m' />
|
||||
<h3 className='text-center'>
|
||||
{t(`certification.title.${Certification.LegacyFullStack}-cert`)}
|
||||
{t(`certification.title.${Certification.LegacyFullStack}`)}
|
||||
</h3>
|
||||
<div>
|
||||
<p>
|
||||
{t('settings.claim-legacy', {
|
||||
cert: t(`certification.title.${Certification.LegacyFullStack}-cert`)
|
||||
cert: t(
|
||||
`certification.title.${Certification.LegacyFullStack}-cert`
|
||||
)
|
||||
})}
|
||||
</p>
|
||||
<ul>
|
||||
<li>{t(`certification.title.${Certification.RespWebDesign}`)}</li>
|
||||
<li>{t(`certification.title.${Certification.JsAlgoDataStruct}`)}</li>
|
||||
<li>
|
||||
{t(`certification.title.${Certification.JsAlgoDataStruct}`)}
|
||||
</li>
|
||||
<li>{t(`certification.title.${Certification.FrontEndDevLibs}`)}</li>
|
||||
<li>{t(`certification.title.${Certification.DataVis}`)}</li>
|
||||
<li>{t(`certification.title.${Certification.BackEndDevApis}`)}</li>
|
||||
@@ -209,6 +214,7 @@ const LegacyFullStack = (props: CertificationSettingsProps) => {
|
||||
</div>
|
||||
<Spacer size='m' />
|
||||
</FullWidthRow>
|
||||
</Element>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -385,7 +391,9 @@ function CertificationSettings(props: CertificationSettingsProps) {
|
||||
<Certification key={cert} certSlug={cert} t={t} />
|
||||
))}
|
||||
<Spacer size='m' />
|
||||
<Element name='legacy-certifications'>
|
||||
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
|
||||
</Element>
|
||||
<LegacyFullStack {...props} />
|
||||
{legacyCertifications.map(cert => (
|
||||
<Certification key={cert} certSlug={cert} t={t} />
|
||||
|
||||
@@ -43,7 +43,11 @@ function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
|
||||
return (
|
||||
<FullWidthRow className='text-center'>
|
||||
<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' />
|
||||
<p>{t('settings.danger.be-careful')}</p>
|
||||
<FullWidthRow>
|
||||
|
||||
@@ -95,7 +95,11 @@ function ExamToken({ email }: ExamTokenProps) {
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
<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>
|
||||
<p>{t('exam-token.note')}</p>
|
||||
<strong>{t('exam-token.invalidation-2')}</strong>
|
||||
|
||||
232
client/src/components/settings/settings-sidebar-nav.tsx
Normal file
232
client/src/components/settings/settings-sidebar-nav.tsx
Normal 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;
|
||||
@@ -29,7 +29,11 @@ class UserToken extends Component<UserTokenProps> {
|
||||
return (
|
||||
<FullWidthRow>
|
||||
<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' />
|
||||
<Panel.Body>
|
||||
<p>{t('user-token.delete-p1')}</p>
|
||||
|
||||
61
e2e/settings-sidenav.spec.ts
Normal file
61
e2e/settings-sidenav.spec.ts
Normal 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 + '$'));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -232,7 +232,7 @@ test.describe('Settings - Certified User', () => {
|
||||
}
|
||||
|
||||
// Danger Zone
|
||||
await expect(page.getByText('Danger Zone')).toBeVisible();
|
||||
await expect(page.getByRole('main').getByText('Danger Zone')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Please be careful. Changes in this section are permanent.'
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Initially', () => {
|
||||
test('should not render', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await expect(
|
||||
page.getByText('User Token', { exact: true })
|
||||
page.getByRole('main').getByText('User Token', { exact: true })
|
||||
).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,9 @@ test.describe('After creating token', () => {
|
||||
|
||||
await page.goto('/settings');
|
||||
// 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(
|
||||
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.'
|
||||
@@ -42,7 +44,7 @@ test.describe('After creating token', () => {
|
||||
|
||||
await alertToBeVisible(page, translations.flash['token-deleted']);
|
||||
await expect(
|
||||
page.getByText('User Token', { exact: true })
|
||||
page.getByRole('main').getByText('User Token', { exact: true })
|
||||
).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user