- {t('settings.for', { username: username })} -
-+ {t('settings.for', { username: username })} +
+- {t(`certification.title.${Certification.LegacyFullStack}-cert`)} -
-- {t('settings.claim-legacy', { - cert: t(`certification.title.${Certification.LegacyFullStack}-cert`) - })} -
--
-
- {t(`certification.title.${Certification.RespWebDesign}`)} -
- {t(`certification.title.${Certification.JsAlgoDataStruct}`)} -
- {t(`certification.title.${Certification.FrontEndDevLibs}`)} -
- {t(`certification.title.${Certification.DataVis}`)} -
- {t(`certification.title.${Certification.BackEndDevApis}`)} -
- {t(`certification.title.${Certification.LegacyInfoSecQa}`)} -
+ {t(`certification.title.${Certification.LegacyFullStack}`)} +
++ {t('settings.claim-legacy', { + cert: t( + `certification.title.${Certification.LegacyFullStack}-cert` + ) + })} +
+-
+
- {t(`certification.title.${Certification.RespWebDesign}`)} +
- + {t(`certification.title.${Certification.JsAlgoDataStruct}`)} + +
- {t(`certification.title.${Certification.FrontEndDevLibs}`)} +
- {t(`certification.title.${Certification.DataVis}`)} +
- {t(`certification.title.${Certification.BackEndDevApis}`)} +
- {t(`certification.title.${Certification.LegacyInfoSecQa}`)} +
+ {t('settings.danger.heading')} +
+{t('settings.danger.be-careful')}
+ {t('exam-token.exam-token')} +
+{t('exam-token.note')}
{t('exam-token.invalidation-2')} diff --git a/client/src/components/settings/settings-sidebar-nav.tsx b/client/src/components/settings/settings-sidebar-nav.tsx new file mode 100644 index 00000000000..68d326620b4 --- /dev/null +++ b/client/src/components/settings/settings-sidebar-nav.tsx @@ -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 ( + + ); +} + +SettingsSidebarNav.displayName = 'SettingsSidebarNav'; + +export default SettingsSidebarNav; diff --git a/client/src/components/settings/user-token.tsx b/client/src/components/settings/user-token.tsx index 0ef7a0da632..8973f82eee6 100644 --- a/client/src/components/settings/user-token.tsx +++ b/client/src/components/settings/user-token.tsx @@ -29,7 +29,11 @@ class UserToken extends Component+ {t('user-token.title')} +
+{t('user-token.delete-p1')}
diff --git a/e2e/settings-sidenav.spec.ts b/e2e/settings-sidenav.spec.ts new file mode 100644 index 00000000000..a830a231990 --- /dev/null +++ b/e2e/settings-sidenav.spec.ts @@ -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 + '$')); + } + }); +}); diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index 1b1e94bd9d9..9abcfe52502 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -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.' diff --git a/e2e/user-token.spec.ts b/e2e/user-token.spec.ts index 6e7c03f9efe..ea1387fd7f8 100644 --- a/e2e/user-token.spec.ts +++ b/e2e/user-token.spec.ts @@ -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(); }); });