mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-29 08:00:43 -04:00
feat(client): migrate to Gatsby v5 and React 18 (#65729)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -8,7 +8,7 @@ const config = {
|
||||
loose: true,
|
||||
modules: false,
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 2,
|
||||
corejs: 3,
|
||||
shippedProposals: true,
|
||||
targets: {
|
||||
browsers: ['>0.25%', 'not dead']
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LocationProvider } from '@gatsbyjs/reach-router';
|
||||
import cookies from 'browser-cookies';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
@@ -16,15 +17,17 @@ const store = createStore();
|
||||
|
||||
export const wrapRootElement = ({ element }) => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<GrowthBookProvider>
|
||||
<AppMountNotifier>
|
||||
<Elements stripe={stripe}>{element}</Elements>
|
||||
</AppMountNotifier>
|
||||
</GrowthBookProvider>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
<LocationProvider>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<GrowthBookProvider>
|
||||
<AppMountNotifier>
|
||||
<Elements stripe={stripe}>{element}</Elements>
|
||||
</AppMountNotifier>
|
||||
</GrowthBookProvider>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</LocationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,6 +39,10 @@ export const wrapPageElement = layoutSelector;
|
||||
|
||||
export const disableCorePrefetching = () => true;
|
||||
|
||||
export const onRouteUpdate = () => {
|
||||
store.dispatch({ type: 'app.routeUpdated' });
|
||||
};
|
||||
|
||||
export const onClientEntry = () => {
|
||||
// Letting the users' browsers expire the cookie seems to have caused issues
|
||||
// for some users. Until we have time to investigate further, we should remove
|
||||
|
||||
@@ -15,13 +15,14 @@ module.exports = {
|
||||
flags: {
|
||||
DEV_SSR: false
|
||||
},
|
||||
trailingSlash: 'ignore',
|
||||
siteMetadata: {
|
||||
title: 'freeCodeCamp',
|
||||
siteUrl: homeLocation
|
||||
},
|
||||
pathPrefix: pathPrefix,
|
||||
plugins: [
|
||||
'gatsby-plugin-pnpm',
|
||||
'gatsby-plugin-pnpm-gatsby-5',
|
||||
{
|
||||
resolve: 'gatsby-plugin-webpack-bundle-analyser-v2',
|
||||
options: {
|
||||
@@ -43,18 +44,6 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-create-client-paths',
|
||||
options: {
|
||||
prefixes: [
|
||||
'/certification/*',
|
||||
'/unsubscribed/*',
|
||||
'/user/*',
|
||||
'/settings/*',
|
||||
'/n/*'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: require.resolve(
|
||||
'../tools/client-plugins/gatsby-source-challenges'
|
||||
|
||||
@@ -59,7 +59,13 @@ exports.createPages = async function createPages({
|
||||
|
||||
const result = await graphql(`
|
||||
{
|
||||
allChallengeNode {
|
||||
allChallengeNode(
|
||||
sort: [
|
||||
{ challenge: { superOrder: ASC } }
|
||||
{ challenge: { order: ASC } }
|
||||
{ challenge: { challengeOrder: ASC } }
|
||||
]
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
challenge {
|
||||
@@ -150,9 +156,9 @@ exports.onCreateWebpackConfig = ({ stage, actions }) => {
|
||||
})
|
||||
];
|
||||
// The monaco editor relies on some browser only globals so should not be
|
||||
// involved in SSR. Also, if the plugin is used during the 'build-html' stage
|
||||
// it overwrites the minfied files with ordinary ones.
|
||||
if (stage !== 'build-html') {
|
||||
// involved in SSR. Also, if the plugin is used during the 'build-html' or
|
||||
// 'develop-html' stage it overwrites the minfied files with ordinary ones.
|
||||
if (stage !== 'build-html' && stage !== 'develop-html') {
|
||||
newPlugins.push(
|
||||
new MonacoWebpackPlugin({ filename: '[name].worker-[contenthash].js' })
|
||||
);
|
||||
@@ -192,15 +198,3 @@ exports.onCreateBabelConfig = ({ actions }) => {
|
||||
name: '@babel/plugin-proposal-export-default-from'
|
||||
});
|
||||
};
|
||||
|
||||
exports.onCreatePage = async ({ page, actions }) => {
|
||||
const { createPage } = actions;
|
||||
// Only update the `/challenges` page.
|
||||
if (page.path.match(/^\/challenges/)) {
|
||||
// page.matchPath is a special key that's used for matching pages
|
||||
// with corresponding routes only on the client.
|
||||
page.matchPath = '/challenges/*';
|
||||
// Update the page.
|
||||
createPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,15 +74,13 @@
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"final-form": "4.20.10",
|
||||
"gatsby": "3.15.0",
|
||||
"gatsby-cli": "3.15.0",
|
||||
"gatsby-plugin-create-client-paths": "3.15.0",
|
||||
"gatsby-plugin-pnpm": "^1.2.10",
|
||||
"gatsby-plugin-postcss": "4.15.0",
|
||||
"gatsby-plugin-react-helmet": "4.15.0",
|
||||
"gatsby": "5.16.0",
|
||||
"gatsby-cli": "5.16.0",
|
||||
"gatsby-plugin-postcss": "6.16.0",
|
||||
"gatsby-plugin-react-helmet": "6.16.0",
|
||||
"gatsby-plugin-remove-serviceworker": "1.0.0",
|
||||
"gatsby-source-filesystem": "3.15.0",
|
||||
"gatsby-transformer-remark": "5.25.1",
|
||||
"gatsby-source-filesystem": "5.16.0",
|
||||
"gatsby-transformer-remark": "6.16.0",
|
||||
"i18next": "25.2.1",
|
||||
"instantsearch.js": "4.75.3",
|
||||
"lodash": "4.17.21",
|
||||
@@ -99,9 +97,9 @@
|
||||
"prop-types": "15.8.1",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"query-string": "7.1.3",
|
||||
"react": "17.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-calendar-heatmap": "1.9.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dom": "18.2.0",
|
||||
"react-final-form": "6.5.9",
|
||||
"react-gtm-module": "2.0.11",
|
||||
"react-helmet": "6.1.0",
|
||||
@@ -110,7 +108,7 @@
|
||||
"react-instantsearch": "7.13.6",
|
||||
"react-instantsearch-core": "7.13.6",
|
||||
"react-monaco-editor": "0.48.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-redux": "8.1.3",
|
||||
"react-reflex": "4.1.0",
|
||||
"react-responsive": "9.0.2",
|
||||
"react-scroll": "1.9.0",
|
||||
@@ -143,8 +141,7 @@
|
||||
"@freecodecamp/eslint-config": "workspace:*",
|
||||
"@freecodecamp/shared": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@total-typescript/ts-reset": "^0.5.0",
|
||||
"@types/canvas-confetti": "^1.6.0",
|
||||
@@ -154,8 +151,8 @@
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/node-fetch": "2",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "17.0.83",
|
||||
"@types/react-dom": "17.0.19",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"@types/react-gtm-module": "2.0.3",
|
||||
"@types/react-helmet": "6.1.11",
|
||||
"@types/react-redux": "7.1.33",
|
||||
@@ -171,18 +168,19 @@
|
||||
"@vitest/ui": "^4.0.15",
|
||||
"autoprefixer": "10.4.17",
|
||||
"babel-plugin-macros": "3.1.0",
|
||||
"core-js": "2.6.12",
|
||||
"core-js": "3.37.1",
|
||||
"dotenv": "16.4.5",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"gatsby-plugin-schema-snapshot": "2.15.0",
|
||||
"gatsby-plugin-schema-snapshot": "4.16.0",
|
||||
"gatsby-plugin-webpack-bundle-analyser-v2": "1.1.32",
|
||||
"gatsby-plugin-pnpm-gatsby-5": "1.2.11",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"joi": "17.12.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"monaco-editor-webpack-plugin": "7.0.1",
|
||||
"monaco-editor-webpack-plugin": "7.1.1",
|
||||
"node-fetch": "2.7.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"readdirp": "3.6.0",
|
||||
"redux-saga-test-plan": "4.0.6",
|
||||
"serve": "13.0.4",
|
||||
|
||||
@@ -454,31 +454,29 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
||||
: 'certification.fulltext'
|
||||
}
|
||||
title={t(`certification.title.${certSlug}`, certTitle)}
|
||||
values={{
|
||||
user: displayName,
|
||||
title: t(`certification.title.${certSlug}`, certTitle),
|
||||
time: certDate.toLocaleString([localeCode, 'en-US'], {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}),
|
||||
completionTime
|
||||
}}
|
||||
>
|
||||
<h3>placeholder</h3>
|
||||
<h1>
|
||||
<strong>{{ user: displayName }}</strong>
|
||||
<strong>{'{{user}}'}</strong>
|
||||
</h1>
|
||||
<h3 data-playwright-test-label='successful-completion'>
|
||||
placeholder
|
||||
</h3>
|
||||
<h1 data-playwright-test-label='certification-title'>
|
||||
<strong>
|
||||
{{
|
||||
title: t(`certification.title.${certSlug}`, certTitle)
|
||||
}}
|
||||
</strong>
|
||||
<strong>{'{{title}}'}</strong>
|
||||
</h1>
|
||||
<h4 data-playwright-test-label='issue-date'>
|
||||
{{
|
||||
time: certDate.toLocaleString([localeCode, 'en-US'], {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}}
|
||||
</h4>
|
||||
<h5 style={{ marginTop: '15px' }}>{{ completionTime }}</h5>
|
||||
<h4 data-playwright-test-label='issue-date'>{'{{time}}'}</h4>
|
||||
<h5 style={{ marginTop: '15px' }}>{'{{completionTime}}'}</h5>
|
||||
</Trans>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -106,8 +106,8 @@ export function ShowUser({
|
||||
<Row className='overflow-fix'>
|
||||
<Col sm={6} smOffset={3} xs={12}>
|
||||
<p>
|
||||
<Trans i18nKey='report.notify-1'>
|
||||
<strong>{{ email: user.email }}</strong>
|
||||
<Trans i18nKey='report.notify-1' values={{ email: user.email }}>
|
||||
<strong>{'{{email}}'}</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t('report.notify-2')}</p>
|
||||
|
||||
@@ -168,13 +168,13 @@ function DonationFormRow({
|
||||
);
|
||||
}
|
||||
|
||||
const MultiTierDonationForm: React.FC<MultiTierDonationFormProps> = ({
|
||||
const MultiTierDonationForm = ({
|
||||
handleProcessing,
|
||||
setShowHeaderAndFooter,
|
||||
isMinimalForm,
|
||||
paymentContext,
|
||||
isAnimationEnabled
|
||||
}) => {
|
||||
}: MultiTierDonationFormProps) => {
|
||||
const replace20With25 = useFeature('replace-20-with-25').on;
|
||||
const [donationAmount, setDonationAmount] = useState(
|
||||
replace20With25 ? defaultTierAmountB : defaultTierAmount
|
||||
|
||||
@@ -30,12 +30,9 @@ const LanguageList = ({ t, navigate }: LanguageListProps): JSX.Element => {
|
||||
const [showList, setShowList] = useState(false);
|
||||
const listButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleClick = (): void => {
|
||||
if (showList) {
|
||||
setShowList(false);
|
||||
return;
|
||||
}
|
||||
setShowList(true);
|
||||
const handleClick = (event: React.MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
setShowList(prev => !prev);
|
||||
};
|
||||
|
||||
const handleClickOutside = () => {
|
||||
|
||||
@@ -31,7 +31,8 @@ const MenuButton = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (): void => {
|
||||
const handleClick = (event: React.MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
if (displayMenu) {
|
||||
hideMenu();
|
||||
return;
|
||||
|
||||
@@ -168,13 +168,11 @@ const useGetAllChallengeData = () => {
|
||||
} = useStaticQuery(graphql`
|
||||
query getBlockNode {
|
||||
allChallengeNode(
|
||||
sort: {
|
||||
fields: [
|
||||
challenge___superOrder
|
||||
challenge___order
|
||||
challenge___challengeOrder
|
||||
]
|
||||
}
|
||||
sort: [
|
||||
{ challenge: { superOrder: ASC } }
|
||||
{ challenge: { order: ASC } }
|
||||
{ challenge: { challengeOrder: ASC } }
|
||||
]
|
||||
) {
|
||||
nodes {
|
||||
challenge {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SuperBlocks } from '@freecodecamp/shared/config/curriculum';
|
||||
|
||||
interface SEOProps {
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SiteData {
|
||||
@@ -42,7 +43,7 @@ interface StructuredData {
|
||||
itemListElement: ListItem[];
|
||||
}
|
||||
|
||||
const SEO: React.FC<SEOProps> = ({ title, children }) => {
|
||||
const SEO = ({ title, children }: SEOProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
site: {
|
||||
|
||||
@@ -50,7 +50,7 @@ exports[`<Honesty /> > <Honesty /> snapshot when isHonest is false > Honesty 1`]
|
||||
<a
|
||||
href="mailto:support@freecodecamp.org"
|
||||
>
|
||||
support@freecodecamp.org
|
||||
{{email}}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@ exports[`<Honesty /> > <Honesty /> snapshot when isHonest is true > HonestyAccep
|
||||
<a
|
||||
href="mailto:support@freecodecamp.org"
|
||||
>
|
||||
support@freecodecamp.org
|
||||
{{email}}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -36,9 +36,9 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
|
||||
<p>{t('settings.danger.delete-p1')}</p>
|
||||
<p>{t('settings.danger.delete-p2')}</p>
|
||||
<p>
|
||||
<Trans i18nKey='settings.danger.delete-p3'>
|
||||
<Trans i18nKey='settings.danger.delete-p3' values={{ email }}>
|
||||
<a href={`mailto:${email}`} title={email}>
|
||||
{{ email }}
|
||||
{'{{email}}'}
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
@@ -31,8 +31,8 @@ const Honesty = ({ isHonest, updateIsHonest }: HonestyProps): JSX.Element => {
|
||||
<p>{t('settings.honesty.p5')}</p>
|
||||
<p>{t('settings.honesty.p6')}</p>
|
||||
<p>
|
||||
<Trans i18nKey='settings.honesty.p7'>
|
||||
<a href={`mailto:${email}`}>{{ email }}</a>
|
||||
<Trans i18nKey='settings.honesty.p7' values={{ email }}>
|
||||
<a href={`mailto:${email}`}>{'{{email}}'}</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import {
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import { Router } from '@gatsbyjs/reach-router';
|
||||
import { withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import ShowProfileOrFourOhFour from '../client-only-routes/show-profile-or-four-oh-four';
|
||||
import FourOhFour from '../components/FourOhFour';
|
||||
|
||||
function FourOhFourPage(): JSX.Element {
|
||||
return (
|
||||
<Router>
|
||||
{/* Error from installing @types/react-helmet and @types/react-redux */}
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<ShowProfileOrFourOhFour path={withPrefix('/:maybeUser')} />
|
||||
|
||||
<FourOhFour default />
|
||||
</Router>
|
||||
);
|
||||
return <FourOhFour />;
|
||||
}
|
||||
|
||||
FourOhFourPage.displayName = 'FourOhFourPage';
|
||||
|
||||
19
client/src/pages/[maybeUser].tsx
Normal file
19
client/src/pages/[maybeUser].tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import React from 'react';
|
||||
import ShowProfileOrFourOhFour from '../client-only-routes/show-profile-or-four-oh-four';
|
||||
|
||||
interface ProfilePageProps {
|
||||
params: {
|
||||
maybeUser: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ProfilePage = ({ params: { maybeUser } }: ProfilePageProps) => {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - Redux connect() provides remaining props
|
||||
<ShowProfileOrFourOhFour maybeUser={maybeUser} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Router } from '@gatsbyjs/reach-router';
|
||||
import { withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import ShowCertification from '../client-only-routes/show-certification';
|
||||
import RedirectHome from '../components/redirect-home';
|
||||
|
||||
import './certification.css';
|
||||
|
||||
function Certification(): JSX.Element {
|
||||
return (
|
||||
<Router>
|
||||
<ShowCertification
|
||||
// Error from installing @types/react-helmet and @types/react-redux
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={withPrefix('/certification/:username/:certSlug')}
|
||||
/>
|
||||
|
||||
<RedirectHome default />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default Certification;
|
||||
29
client/src/pages/certification/[username]/[certSlug].tsx
Normal file
29
client/src/pages/certification/[username]/[certSlug].tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import React from 'react';
|
||||
import ShowCertification from '../../../client-only-routes/show-certification';
|
||||
import '../certification.css';
|
||||
|
||||
interface CertificationPageProps {
|
||||
params: {
|
||||
username: string;
|
||||
certSlug: string;
|
||||
};
|
||||
location: {
|
||||
pathname: string;
|
||||
};
|
||||
}
|
||||
|
||||
const CertificationPage = ({ params, location }: CertificationPageProps) => {
|
||||
const { username, certSlug } = params;
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - Redux connect() provides remaining props
|
||||
<ShowCertification
|
||||
username={username}
|
||||
certSlug={certSlug}
|
||||
location={location}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificationPage;
|
||||
7
client/src/pages/certification/index.tsx
Normal file
7
client/src/pages/certification/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import RedirectHome from '../../components/redirect-home';
|
||||
|
||||
// Redirect to home if no username/certSlug provided
|
||||
const Certification = () => <RedirectHome />;
|
||||
|
||||
export default Certification;
|
||||
@@ -1,59 +1,36 @@
|
||||
import {
|
||||
createHistory,
|
||||
createMemorySource,
|
||||
LocationProvider
|
||||
} from '@gatsbyjs/reach-router';
|
||||
import { render } from '@testing-library/react';
|
||||
import { navigate, withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import Challenges from './challenges';
|
||||
import Challenges from './challenges/index';
|
||||
import ChallengesRedirect from './challenges/[...]';
|
||||
|
||||
describe('Challenges', () => {
|
||||
// Source: https://testing-library.com/docs/example-reach-router/
|
||||
function renderWithRouterWrapper({
|
||||
route = '/',
|
||||
history = createHistory(createMemorySource(route))
|
||||
} = {}) {
|
||||
return {
|
||||
...render(
|
||||
<LocationProvider history={history}>
|
||||
<Challenges />
|
||||
</LocationProvider>
|
||||
),
|
||||
// adding `history` to the returned utilities to allow us
|
||||
// to reference it in our tests (just try to avoid using
|
||||
// this to test implementation details).
|
||||
history
|
||||
};
|
||||
}
|
||||
|
||||
const challenges = withPrefix('/challenges');
|
||||
const learn = withPrefix('/learn');
|
||||
|
||||
it('should handle redirect to /learn', () => {
|
||||
renderWithRouterWrapper({ route: challenges });
|
||||
render(<Challenges />);
|
||||
|
||||
expect(navigate).toHaveBeenLastCalledWith(learn);
|
||||
});
|
||||
|
||||
it('should handle redirect to /learn/:super-block', () => {
|
||||
renderWithRouterWrapper({ route: `${challenges}/super-block` });
|
||||
render(<ChallengesRedirect params={{ '*': 'super-block' }} />);
|
||||
|
||||
expect(navigate).toHaveBeenLastCalledWith(`${learn}/super-block`);
|
||||
});
|
||||
|
||||
it('should handle redirect to /learn/:super-block/:block', () => {
|
||||
renderWithRouterWrapper({ route: `${challenges}/super-block/block` });
|
||||
render(<ChallengesRedirect params={{ '*': 'super-block/block' }} />);
|
||||
|
||||
expect(navigate).toHaveBeenLastCalledWith(`${learn}/super-block/block`);
|
||||
});
|
||||
|
||||
it('should handle redirect to /learn/:super-block/:block/:challenge', () => {
|
||||
renderWithRouterWrapper({
|
||||
route: `${challenges}/super-block/block/challenge`
|
||||
});
|
||||
render(
|
||||
<ChallengesRedirect params={{ '*': 'super-block/block/challenge' }} />
|
||||
);
|
||||
|
||||
expect(navigate).toHaveBeenLastCalledWith(
|
||||
`${learn}/super-block/block/challenge`
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// This exists purely to redirect legacy challenge paths to /learn that could
|
||||
// exist in the web (posts, url shares, etc).
|
||||
|
||||
import { Router, RouteComponentProps } from '@gatsbyjs/reach-router';
|
||||
import { navigate, withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import toLearnPath from '../utils/to-learn-path';
|
||||
|
||||
type RouteComponentPropsExtended = RouteComponentProps & {
|
||||
block?: string;
|
||||
challenge?: string;
|
||||
superBlock?: string;
|
||||
};
|
||||
|
||||
function Redirect(props: RouteComponentPropsExtended): null {
|
||||
if (typeof window !== 'undefined') {
|
||||
void navigate(toLearnPath(props));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function Challenges(): JSX.Element {
|
||||
return (
|
||||
<Router basepath={withPrefix('/challenges')}>
|
||||
<Redirect path='/:superBlock/' />
|
||||
<Redirect path='/:superBlock/:block/' />
|
||||
<Redirect path='/:superBlock/:block/:challenge' />
|
||||
<Redirect default={true} />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
Challenges.displayName = 'Challenges';
|
||||
|
||||
export default Challenges;
|
||||
26
client/src/pages/challenges/[...].tsx
Normal file
26
client/src/pages/challenges/[...].tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
// This exists purely to redirect legacy challenge paths to /learn
|
||||
|
||||
import { navigate } from 'gatsby';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import toLearnPath from '../../utils/to-learn-path';
|
||||
|
||||
interface ChallengesRedirectProps {
|
||||
params: {
|
||||
'*': string;
|
||||
};
|
||||
}
|
||||
|
||||
const ChallengesRedirect = ({ params }: ChallengesRedirectProps) => {
|
||||
useEffect(() => {
|
||||
const pathSegments = params['*'].split('/').filter(Boolean);
|
||||
const [superBlock, block, challenge] = pathSegments;
|
||||
|
||||
void navigate(toLearnPath({ superBlock, block, challenge }));
|
||||
}, [params]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ChallengesRedirect;
|
||||
15
client/src/pages/challenges/index.tsx
Normal file
15
client/src/pages/challenges/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// This exists purely to redirect legacy challenge paths to /learn that could
|
||||
// exist in the web (posts, url shares, etc).
|
||||
|
||||
import { navigate } from 'gatsby';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const Challenges = () => {
|
||||
useEffect(() => {
|
||||
void navigate('/learn');
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Challenges;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Router } from '@gatsbyjs/reach-router';
|
||||
import { withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import ShowSettings from '../client-only-routes/show-settings';
|
||||
import RedirectHome from '../components/redirect-home';
|
||||
|
||||
function Settings(): JSX.Element {
|
||||
return (
|
||||
<Router>
|
||||
<ShowSettings path={withPrefix('/settings')} />
|
||||
|
||||
<RedirectHome default />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
Settings.displayName = 'Settings';
|
||||
|
||||
export default Settings;
|
||||
7
client/src/pages/settings/[...].tsx
Normal file
7
client/src/pages/settings/[...].tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import React from 'react';
|
||||
import ShowSettings from '../../client-only-routes/show-settings';
|
||||
|
||||
const Settings = () => <ShowSettings />;
|
||||
|
||||
export default Settings;
|
||||
6
client/src/pages/settings/index.tsx
Normal file
6
client/src/pages/settings/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import ShowSettings from '../../client-only-routes/show-settings';
|
||||
|
||||
const Settings = () => <ShowSettings />;
|
||||
|
||||
export default Settings;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Router } from '@gatsbyjs/reach-router';
|
||||
import { withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import ShowUnsubscribed from '../client-only-routes/show-unsubscribed';
|
||||
import RedirectHome from '../components/redirect-home';
|
||||
|
||||
function Unsubscribed(): JSX.Element {
|
||||
return (
|
||||
<Router>
|
||||
<ShowUnsubscribed path={withPrefix('/unsubscribed/:unsubscribeId')} />
|
||||
|
||||
<ShowUnsubscribed path={withPrefix('/unsubscribed')} />
|
||||
|
||||
<RedirectHome default />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
Unsubscribed.displayName = 'Unsubscribed';
|
||||
|
||||
export default Unsubscribed;
|
||||
17
client/src/pages/unsubscribed/[unsubscribeId].tsx
Normal file
17
client/src/pages/unsubscribed/[unsubscribeId].tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import React from 'react';
|
||||
import ShowUnsubscribed from '../../client-only-routes/show-unsubscribed';
|
||||
|
||||
interface UnsubscribedWithIdProps {
|
||||
params: {
|
||||
unsubscribeId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const UnsubscribedWithId = ({
|
||||
params: { unsubscribeId }
|
||||
}: UnsubscribedWithIdProps) => (
|
||||
<ShowUnsubscribed unsubscribeId={unsubscribeId} />
|
||||
);
|
||||
|
||||
export default UnsubscribedWithId;
|
||||
6
client/src/pages/unsubscribed/index.tsx
Normal file
6
client/src/pages/unsubscribed/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import ShowUnsubscribed from '../../client-only-routes/show-unsubscribed';
|
||||
|
||||
const Unsubscribed = () => <ShowUnsubscribed />;
|
||||
|
||||
export default Unsubscribed;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Router } from '@gatsbyjs/reach-router';
|
||||
import { withPrefix } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import ShowUser from '../client-only-routes/show-user';
|
||||
import RedirectHome from '../components/redirect-home';
|
||||
|
||||
function User(): JSX.Element {
|
||||
return (
|
||||
<Router>
|
||||
{/* @ts-expect-error Adding path property breaks username typing */}
|
||||
<ShowUser path={withPrefix('/user/:username/report-user')} />
|
||||
|
||||
<RedirectHome default />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
User.displayName = 'User';
|
||||
|
||||
export default User;
|
||||
14
client/src/pages/user/[username]/report-user/index.tsx
Normal file
14
client/src/pages/user/[username]/report-user/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ShowUser from '../../../../client-only-routes/show-user';
|
||||
|
||||
interface ReportUserPageProps {
|
||||
params: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ReportUserPage = ({ params: { username } }: ReportUserPageProps) => (
|
||||
<ShowUser username={username} />
|
||||
);
|
||||
|
||||
export default ReportUserPage;
|
||||
7
client/src/pages/user/index.tsx
Normal file
7
client/src/pages/user/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import RedirectHome from '../../components/redirect-home';
|
||||
|
||||
// Redirect to home if no username provided
|
||||
const User = () => <RedirectHome />;
|
||||
|
||||
export default User;
|
||||
@@ -20,6 +20,7 @@ export const actionTypes = createTypes(
|
||||
'onlineStatusChange',
|
||||
'serverStatusChange',
|
||||
'resetUserData',
|
||||
'routeUpdated',
|
||||
'tryToShowDonationModal',
|
||||
'startExam',
|
||||
'stopExam',
|
||||
|
||||
@@ -84,6 +84,7 @@ export const reportUserComplete = createAction(actionTypes.reportUserComplete);
|
||||
export const reportUserError = createAction(actionTypes.reportUserError);
|
||||
|
||||
export const resetUserData = createAction(actionTypes.resetUserData);
|
||||
export const routeUpdated = createAction(actionTypes.routeUpdated);
|
||||
|
||||
export const showCert = createAction(actionTypes.showCert);
|
||||
export const showCertComplete = createAction(actionTypes.showCertComplete);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { withPrefix } from 'gatsby';
|
||||
import { navigate } from '@gatsbyjs/reach-router';
|
||||
import { navigate } from 'gatsby';
|
||||
import { call, put, take, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { createFlashMessage } from '../../components/Flash/redux';
|
||||
@@ -18,12 +17,12 @@ function* deleteAccountSaga() {
|
||||
message: FlashMessages.AccountDeleted
|
||||
})
|
||||
);
|
||||
// navigate before signing out, since /settings will attempt to sign users
|
||||
// back in. Using reach-router's navigate because gatsby's resolves after
|
||||
// the call. This would allow resetUserData to take place while the user is
|
||||
// still on /settings.
|
||||
yield call(navigate, withPrefix('/learn'));
|
||||
// remove current user information from application state
|
||||
// Navigate before signing out, since /settings will attempt to sign users
|
||||
// back in if resetUserData fires while still on /settings.
|
||||
void navigate('/learn');
|
||||
// Wait for Gatsby to complete the route transition before clearing user
|
||||
// data, ensuring /settings is unmounted and won't re-authenticate.
|
||||
yield take(appTypes.routeUpdated);
|
||||
yield put(resetUserData());
|
||||
} catch (e) {
|
||||
yield put(deleteAccountError(e));
|
||||
|
||||
@@ -177,7 +177,9 @@ function ShowCodeAlly({
|
||||
...challengePaths
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
// This effect should be run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -205,7 +205,9 @@ function ShowExam(props: ShowExamProps) {
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
|
||||
return () => {
|
||||
cleanUp();
|
||||
|
||||
@@ -131,7 +131,9 @@ const ShowFillInTheBlank = ({
|
||||
...challengePaths
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
// This effect should be run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -145,7 +145,9 @@ const ShowGeneric = ({
|
||||
...challengePaths
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
// This effect should be run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, globalHistory } from '@gatsbyjs/reach-router';
|
||||
import { useLocation } from '@gatsbyjs/reach-router';
|
||||
|
||||
interface Props {
|
||||
onWindowClose: (event: BeforeUnloadEvent) => void;
|
||||
onHistoryChange: (targetPathname: string) => void;
|
||||
onHistoryChange: (targetPathname: string) => boolean;
|
||||
}
|
||||
|
||||
export const usePageLeave = ({ onWindowClose, onHistoryChange }: Props) => {
|
||||
@@ -11,22 +11,39 @@ export const usePageLeave = ({ onWindowClose, onHistoryChange }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('beforeunload', onWindowClose);
|
||||
// Push a dummy state so that navigating back will restore the current page,
|
||||
// allowing us to manually handle navigation.
|
||||
window.history.pushState({}, curLocation.pathname);
|
||||
|
||||
// This is a workaround as @gatsbyjs/reach-router doesn't support blocking history change.
|
||||
// https://github.com/reach/router/issues/464
|
||||
const unlistenHistory = globalHistory.listen(({ action, location }) => {
|
||||
const isBack = action === 'POP';
|
||||
const isRouteChanged =
|
||||
action === 'PUSH' && location.pathname !== curLocation.pathname;
|
||||
const handlePopState = () => {
|
||||
// The argument should be an empty string, so that onHistoryChange knows
|
||||
// to use the default navigation target
|
||||
onHistoryChange('');
|
||||
};
|
||||
|
||||
if (isBack || isRouteChanged) {
|
||||
onHistoryChange(location.pathname);
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
|
||||
const handleLinkClick = (event: MouseEvent) => {
|
||||
const anchor = (event.target as HTMLElement).closest('a');
|
||||
if (!anchor) return;
|
||||
|
||||
const href = anchor.getAttribute('href');
|
||||
if (!href || !href.startsWith('/')) return;
|
||||
if (href === curLocation.pathname) return;
|
||||
|
||||
const blocked = onHistoryChange(href);
|
||||
if (blocked) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleLinkClick, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', onWindowClose);
|
||||
unlistenHistory();
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
document.removeEventListener('click', handleLinkClick, true);
|
||||
};
|
||||
}, [onWindowClose, onHistoryChange, curLocation]);
|
||||
};
|
||||
|
||||
@@ -116,7 +116,9 @@ function MsTrophy(props: MsTrophyProps) {
|
||||
...challengePaths
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
// This effect should be run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -130,7 +130,9 @@ const ShowBackEnd = (props: BackEndProps) => {
|
||||
...challengePaths
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -105,7 +105,9 @@ const ShowFrontEndProject = (props: ProjectProps) => {
|
||||
...challengePaths
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
// This effect should be run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { graphql } from 'gatsby';
|
||||
import { graphql, navigate } from 'gatsby';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { ObserveKeys } from 'react-hotkeys';
|
||||
@@ -7,7 +7,6 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useLocation, navigate as reachNavigate } from '@gatsbyjs/reach-router';
|
||||
import {
|
||||
Container,
|
||||
Col,
|
||||
@@ -116,8 +115,6 @@ const ShowQuiz = ({
|
||||
closeFinishQuizModal
|
||||
}: ShowQuizProps) => {
|
||||
const { t } = useTranslation();
|
||||
const curLocation = useLocation();
|
||||
|
||||
const container = useRef<HTMLElement | null>(null);
|
||||
|
||||
// Campers are not allowed to change their answers once the quiz is submitted.
|
||||
@@ -220,7 +217,9 @@ const ShowQuiz = ({
|
||||
...challengePaths
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
container.current?.focus();
|
||||
// hack to ensure the container is focused after the component mounts
|
||||
// and Gatsby doesn't interfere with the focus.
|
||||
requestAnimationFrame(() => container.current?.focus());
|
||||
// This effect should be run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -249,7 +248,7 @@ const ShowQuiz = ({
|
||||
|
||||
const handleExitQuizModalBtnClick = () => {
|
||||
exitConfirmed.current = true;
|
||||
void reachNavigate(exitPathname || '/learn');
|
||||
void navigate(exitPathname || '/learn', { replace: true });
|
||||
closeExitQuizModal();
|
||||
};
|
||||
|
||||
@@ -262,29 +261,26 @@ const ShowQuiz = ({
|
||||
);
|
||||
|
||||
const onHistoryChange = useCallback(
|
||||
(targetPathname: string) => {
|
||||
(targetPathname: string): boolean => {
|
||||
// We don't block navigation in the following cases.
|
||||
// - When campers have submitted the quiz:
|
||||
// - If they don't pass, the Finish Quiz button is disabled, there isn't anything for them to do other than leaving the page
|
||||
// - If they pass, the Submit-and-go button shows up, and campers should be allowed to leave the page
|
||||
// - When they have clicked the exit button on the exit modal
|
||||
if (hasSubmitted || exitConfirmed.current) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const newPathname = targetPathname.startsWith('/learn')
|
||||
? blockHashSlug
|
||||
: targetPathname;
|
||||
// For link clicks, save the target pathname. For back button
|
||||
// (empty targetPathname), keep the default (i.e. blockHashSlug).
|
||||
if (targetPathname) {
|
||||
setExitPathname(targetPathname);
|
||||
}
|
||||
|
||||
// Save the pathname of the page the user wants to navigate to before we block the navigation.
|
||||
setExitPathname(newPathname);
|
||||
|
||||
// We need to use Reach Router, because the pathname is already prefixed
|
||||
// with the language and Gatsby's navigate will prefix it again.
|
||||
void reachNavigate(`${curLocation.pathname}`);
|
||||
openExitQuizModal();
|
||||
return true;
|
||||
},
|
||||
[curLocation.pathname, hasSubmitted, openExitQuizModal, blockHashSlug]
|
||||
[hasSubmitted, openExitQuizModal]
|
||||
);
|
||||
|
||||
usePageLeave({
|
||||
|
||||
@@ -73,7 +73,7 @@ export const query = graphql`
|
||||
html
|
||||
}
|
||||
allChallengeNode(
|
||||
sort: { fields: [challenge___challengeOrder] }
|
||||
sort: { challenge: { challengeOrder: ASC } }
|
||||
filter: { challenge: { block: { eq: $block } } }
|
||||
limit: 1
|
||||
) {
|
||||
|
||||
@@ -358,13 +358,11 @@ export default connect(
|
||||
export const query = graphql`
|
||||
query SuperBlockIntroPageQuery {
|
||||
allChallengeNode(
|
||||
sort: {
|
||||
fields: [
|
||||
challenge___superOrder
|
||||
challenge___order
|
||||
challenge___challengeOrder
|
||||
]
|
||||
}
|
||||
sort: [
|
||||
{ challenge: { superOrder: ASC } }
|
||||
{ challenge: { order: ASC } }
|
||||
{ challenge: { challengeOrder: ASC } }
|
||||
]
|
||||
) {
|
||||
nodes {
|
||||
challenge {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { type ReactElement } from 'react';
|
||||
import React, { type ReactElement, type ReactNode } from 'react';
|
||||
import { render as rtlRender } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
function render(ui: ReactElement, store: Store) {
|
||||
function Wrapper({ children }: { children: ReactElement }) {
|
||||
function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
return rtlRender(ui, { wrapper: Wrapper });
|
||||
|
||||
@@ -7,13 +7,6 @@ test.describe('Public profile certifications', () => {
|
||||
}) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
// If you build the client locally, delete the button click below.
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: /View.+Certification/ })
|
||||
).toHaveCount(25);
|
||||
@@ -24,11 +17,6 @@ test.describe('Public profile certifications', () => {
|
||||
}) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
|
||||
await page.getByLabel('Username').fill('CertifiedBoozer');
|
||||
@@ -38,13 +26,6 @@ test.describe('Public profile certifications', () => {
|
||||
);
|
||||
await page.goto('/certifiedboozer');
|
||||
|
||||
// If you build the client locally, delete the button click below.
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.waitForURL('/certifiedboozer');
|
||||
await expect(
|
||||
page.getByRole('link', { name: /View.+Certification/ })
|
||||
|
||||
@@ -66,12 +66,6 @@ test.describe('Completed project preview', () => {
|
||||
test('it should be viewable on the timeline', async ({ page }) => {
|
||||
await page.goto('/developmentuser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '@developmentuser' })
|
||||
).toBeVisible();
|
||||
|
||||
@@ -4,10 +4,6 @@ import translations from '../client/i18n/locales/english/translations.json';
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
|
||||
@@ -17,12 +17,6 @@ test.describe('Add Experience Item', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/developmentuser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
|
||||
await expect(async () => {
|
||||
|
||||
@@ -4,12 +4,6 @@ test.describe('Picture input field', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
|
||||
@@ -19,10 +19,6 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
|
||||
@@ -16,12 +16,6 @@ test.describe('Add Portfolio Item', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
|
||||
// Will check if the portfolio button is hydrated correctly with different intervals.
|
||||
|
||||
@@ -88,13 +88,6 @@ test.describe('Profile component', () => {
|
||||
test.describe('when viewing my own profile', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
// If you build the client locally, delete the button click below.
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
});
|
||||
|
||||
test('renders the camper profile correctly', async ({ page }) => {
|
||||
@@ -173,13 +166,6 @@ test.describe('Profile component', () => {
|
||||
test.describe("when viewing someone else's profile", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/publicUser');
|
||||
|
||||
// If you build the client locally, delete the button click below.
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('while logged in', () => {
|
||||
|
||||
@@ -236,14 +236,40 @@ test.describe('Quiz challenge', () => {
|
||||
// Wait for the page content to render
|
||||
await expect(page.getByRole('radiogroup')).toHaveCount(20);
|
||||
|
||||
await page.getByRole('link', { name: 'Basic HTML Quiz' }).click();
|
||||
// navigate to /learn
|
||||
await page.getByTestId('header-universal-nav-logo').click();
|
||||
|
||||
await expect(page.getByRole('dialog', { name: 'Exit Quiz' })).toBeVisible();
|
||||
await page
|
||||
.getByRole('button', { name: 'Yes, I want to leave the quiz' })
|
||||
.click();
|
||||
|
||||
await page.waitForURL('/learn/responsive-web-design-v9/#quiz-basic-html');
|
||||
await expect(page).toHaveURL(allowTrailingSlash('/learn'));
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Welcome back, Full Stack User.' })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show a confirm exit modal when user presses the back button', async ({
|
||||
page
|
||||
}) => {
|
||||
const blockPath = '/learn/responsive-web-design-v9/#quiz-basic-html';
|
||||
|
||||
await page.goto(blockPath);
|
||||
await page.goto(quizPath);
|
||||
|
||||
await expect(page.getByRole('radiogroup')).toHaveCount(20);
|
||||
|
||||
await page.goBack();
|
||||
|
||||
await expect(page).toHaveURL(allowTrailingSlash(quizPath));
|
||||
await expect(page.getByRole('dialog', { name: 'Exit Quiz' })).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'Yes, I want to leave the quiz' })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(blockPath);
|
||||
await expect(
|
||||
page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' })
|
||||
).toBeVisible();
|
||||
|
||||
@@ -15,11 +15,6 @@ test('should be possible to report a user from their profile page', async ({
|
||||
}) => {
|
||||
await page.goto('/twaha');
|
||||
|
||||
// If you build the client locally, delete the button click below.
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByText("Flag This User's Account for Abuse").click();
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -9,10 +9,6 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
@@ -64,10 +60,6 @@ test('Should allow empty string in any field in about settings', async ({
|
||||
|
||||
await page.reload();
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
await expect(nameInput).toHaveValue('');
|
||||
await expect(locationInput).toHaveValue('');
|
||||
|
||||
@@ -26,12 +26,6 @@ test.describe('Username Settings Validation', () => {
|
||||
execSync('node ../tools/scripts/seed/seed-demo-user --certified-user');
|
||||
await page.goto(`/certifieduser`);
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
|
||||
9076
pnpm-lock.yaml
generated
9076
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,9 @@
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "gatsby-node.js",
|
||||
"peerDependencies": {
|
||||
"gatsby": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@freecodecamp/eslint-config": "workspace:*",
|
||||
"eslint": "^9.39.1"
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "gatsby-node.js",
|
||||
"peerDependencies": {
|
||||
"gatsby": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@freecodecamp/eslint-config": "workspace:*",
|
||||
"@freecodecamp/curriculum": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user