feat: add independent lowerjaw in labs (#65785)

This commit is contained in:
Ahmad Abdolsaheb
2026-03-09 14:38:31 +03:00
committed by GitHub
parent 7510f608c3
commit 4ea5ae56cb
24 changed files with 457 additions and 150 deletions
@@ -518,7 +518,7 @@
"ms-link": "Microsoft Link",
"submit-and-go": "Submit and go to my next challenge",
"congratulations": "Congratulations, your code passes. Submit your code to continue.",
"congratulations-code-passes": "Congratulations. Your code passes.",
"congratulations-code-passes": "Congratulations. Your code passes.",
"i-completed": "I've completed this challenge",
"example-code": "Example Code",
"test-output": "Your test output will go here",
@@ -556,7 +556,7 @@
"contact-support-mistake": "If you think there has been a mistake, please contact us at donors@freecodecamp.org",
"editor-tabs": {
"code": "Code",
"tests": "Tests",
"tests": "Tests:",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
@@ -7,6 +7,7 @@ interface ProgressInnerProps {
completedPercent: number;
title: string;
meta: string;
minified?: boolean;
}
const easing = BezierEasing(0.2, 0.5, 0.4, 1);
@@ -36,7 +37,8 @@ function useIsInViewport(ref: React.RefObject<HTMLDivElement>) {
function ProgressInner({
completedPercent,
title,
meta
meta,
minified
}: ProgressInnerProps): JSX.Element {
const [shownPercent, setShownPercent] = useState(0);
const [lastShownPercent, setLastShownPercent] = useState(0);
@@ -94,6 +96,24 @@ function ProgressInner({
};
}, []);
if (minified) {
return (
<>
<div className='progress-header'>
<div>{title}</div>
<div>{meta}</div>
</div>
<div
className='progress-bar-wrap'
aria-hidden='true'
ref={progressInnerWrap}
>
<ProgressBar now={shownPercent} />
</div>
</>
);
}
return (
<>
<div className='completion-block-name'>{title}</div>
+4 -1
View File
@@ -56,6 +56,7 @@ type PropsFromRedux = ConnectedProps<typeof connector>;
interface ProgressProps extends PropsFromRedux {
t: TFunction;
minified?: boolean;
}
function Progress({
currentBlockIds,
@@ -65,7 +66,8 @@ function Progress({
challengeType,
completedChallengesInBlock,
completedPercent,
t
t,
minified
}: ProgressProps): JSX.Element {
useFetchAllCurriculumData(); // needed to compute completedPercent
let blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
@@ -103,6 +105,7 @@ function Progress({
title={blockTitle}
meta={meta}
completedPercent={completedPercent}
minified={minified}
/>
</div>
);
@@ -315,8 +315,12 @@ function ShowClassic({
// AB testing Pre-fetch in the Spanish locale
const isPreFetchEnabled = useFeature('prefetch_ab_test').on;
// Independent lower jaw is only enabled for desktop workshops.
const showIndependentLowerJaw = hasEditableBoundaries && !isMobile;
// Independent lower jaw is only enabled for desktop.
const showIndependentLowerJaw = !isMobile;
const showSidePanelTests = isMobile || !hasEditableBoundaries;
// Show test
useEffect(() => {
if (isPreFetchEnabled && envData.clientLocale === 'espanol') {
@@ -428,7 +432,7 @@ function ShowClassic({
instructionsPanelRef={instructionsPanelRef}
toolPanel={toolPanel}
hasDemo={hasDemo}
showIndependentLowerJaw={showIndependentLowerJaw}
showSidePanelTests={showSidePanelTests}
/>
);
};
@@ -470,7 +474,7 @@ function ShowClassic({
>
<LearnLayout>
<Helmet title={windowTitle} />
{isMobile && (
{isMobile ? (
<MobileLayout
editor={renderEditor({
isMobileLayout: true,
@@ -502,8 +506,7 @@ function ShowClassic({
updateUsingKeyboardInTablist={updateUsingKeyboardInTablist}
usesMultifileEditor={usesMultifileEditor}
/>
)}
{!isMobile && (
) : (
<DesktopLayout
challengeFiles={challengeFiles}
challengeType={challengeType}
@@ -514,7 +517,7 @@ function ShowClassic({
hasEditableBoundaries={hasEditableBoundaries}
hasPreview={hasPreview}
instructions={renderInstructionsPanel({
toolPanel: <ToolPanel guideUrl={guideUrl} videoUrl={videoUrl} />,
toolPanel: null,
hasDemo: demoType === 'onClick'
})}
isDailyCodingChallenge={isDailyCodingChallenge}
@@ -546,6 +549,8 @@ function ShowClassic({
challengeTitle={title}
challengeBlock={block}
superBlock={superBlock}
guideUrl={guideUrl}
videoUrl={videoUrl}
/>
<VideoModal videoUrl={videoUrl} />
<ResetModal
@@ -46,6 +46,15 @@
.progress-bar-wrap {
width: 100%;
position: relative;
border: 1px solid var(--quaternary-color);
}
.progress-header {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
font-size: 0.9rem;
}
.progress-bar-background {
@@ -57,6 +66,7 @@
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
}
@@ -6,7 +6,7 @@ import { Button, FormControl, Modal, Spacer } from '@freecodecamp/ui';
import { t } from 'i18next';
import envData from '../../../../config/env.json';
import { createQuestion, closeModal } from '../redux/actions';
import { createQuestion, closeModal, openModal } from '../redux/actions';
import { isHelpModalOpenSelector } from '../redux/selectors';
import './help-modal.css';
@@ -19,6 +19,9 @@ interface HelpModalProps {
challengeTitle: string;
challengeBlock: string;
superBlock: string;
guideUrl?: string;
videoUrl?: string;
openVideoModal: () => void;
}
const { forumLocation } = envData;
@@ -31,7 +34,11 @@ const mapStateToProps = (state: unknown) => ({
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{ createQuestion, closeHelpModal: () => closeModal('help') },
{
createQuestion,
closeHelpModal: () => closeModal('help'),
openVideoModal: () => openModal('video')
},
dispatch
);
@@ -43,7 +50,6 @@ export const generateSearchLink = (
const titleText = t(`intro:${superBlock}.blocks.${block}.title`);
const selector = 'in:title';
const query = encodeURIComponent(`${titleText} - ${title} ${selector}`);
const search = `${forumLocation}/search?q=${query}`;
return search;
};
@@ -99,7 +105,10 @@ function HelpModal({
isOpen,
challengeBlock,
superBlock,
challengeTitle
challengeTitle,
guideUrl,
videoUrl,
openVideoModal
}: HelpModalProps): JSX.Element {
const { t } = useTranslation();
const [showHelpForm, setShowHelpForm] = useState(false);
@@ -136,6 +145,11 @@ function HelpModal({
resetFormValues();
};
const handleOpenVideo = () => {
openVideoModal();
handleClose();
};
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
@@ -149,13 +163,17 @@ function HelpModal({
closeHelpModal();
};
const hintUrl = guideUrl
? guideUrl
: generateSearchLink(challengeTitle, challengeBlock, superBlock);
if (isOpen) {
callGA({ event: 'pageview', pagePath: '/help-modal' });
}
return (
<Modal onClose={handleClose} open={!!isOpen}>
<Modal.Header closeButtonClassNames='close'>
{t('buttons.ask-for-help')}
{t('buttons.get-help')}
</Modal.Header>
<Modal.Body>
{showHelpForm ? (
@@ -278,12 +296,38 @@ function HelpModal({
</Trans>
</p>
</div>
<Button
block={true}
size='large'
variant='primary'
href={hintUrl}
target='_blank'
rel='noopener noreferrer'
data-playwright-test-label='get-hint-modal-button'
>
{t('buttons.get-hint')}
</Button>
<Spacer size='xxs' />
{videoUrl && (
<>
<Button
block={true}
size='large'
variant='primary'
onClick={handleOpenVideo}
data-playwright-test-label='watch-a-video-modal-button'
>
{t('buttons.watch-video')}
</Button>
<Spacer size='xxs' />
</>
)}
<Button
block={true}
size='large'
variant='primary'
onClick={() => setShowHelpForm(true)}
data-playwright-test-label='create-post-modal-button'
>
{t('buttons.create-post')}
</Button>
@@ -292,7 +336,7 @@ function HelpModal({
block={true}
size='large'
variant='primary'
onClick={closeHelpModal}
onClick={handleClose}
>
{t('buttons.cancel')}
</Button>
@@ -8,12 +8,13 @@
.independent-lower-jaw .hint-container {
background-color: var(--background-quaternary);
display: flex;
flex-direction: row;
flex-direction: column;
justify-content: space-between;
gap: 10px;
padding: 12px;
position: absolute;
bottom: 100%;
width: 95%;
width: 97%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 12px;
@@ -22,6 +23,21 @@
animation: jaw-hint-fade-in 0.3s ease forwards;
}
.independent-lower-jaw .hint-container .hint-header {
display: flex;
justify-content: space-between;
flex-direction: row;
}
.independent-lower-jaw .hint-container .hint-header svg {
height: 1.4rem;
width: 1.4rem;
}
.independent-lower-jaw .hint-container .hint-body p:last-child {
margin-bottom: 0;
}
@keyframes jaw-hint-fade-in {
from {
opacity: 0;
@@ -57,7 +73,7 @@
display: flex;
justify-content: center;
align-items: center;
border: none;
border: 1px solid var(--quaternary-color);
}
.independent-lower-jaw .action-row-right button {
@@ -122,6 +138,12 @@
.share-button-wrapper {
display: flex;
justify-content: start;
justify-content: end;
margin-top: 10px;
gap: 10px;
}
.share-button-wrapper a {
border: 1px solid var(--quaternary-color);
padding: 2px 6px;
}
@@ -10,6 +10,11 @@ import { createStore } from '../../../redux/create-store';
import { mockCurriculumData } from '../utils/__fixtures__/curriculum-data';
import { render } from '../../../../utils/test-utils';
vi.mock('../../../components/Progress');
vi.mock('../utils/fetch-all-curriculum-data', () => ({
useSubmit: () => vi.fn()
}));
const baseChallengeMeta: ChallengeMeta = {
block: 'test-block',
id: 'test-challenge-id',
@@ -21,12 +26,11 @@ const baseChallengeMeta: ChallengeMeta = {
};
const passingTests: Test[] = [{ pass: true, text: 'test', testString: 'test' }];
const baseProps = {
openHelpModal: vi.fn(),
openResetModal: vi.fn(),
executeChallenge: vi.fn(),
submitChallenge: vi.fn(),
saveChallenge: vi.fn(),
tests: passingTests,
isSignedIn: true,
challengeMeta: baseChallengeMeta,
@@ -2,7 +2,17 @@ import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { useTranslation } from 'react-i18next';
import { Button } from '@freecodecamp/ui';
import { Button, Spacer } from '@freecodecamp/ui';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faLightbulb,
faClose,
faZap,
faSave,
faClockRotateLeft,
faRotateLeft
} from '@fortawesome/free-solid-svg-icons';
import Progress from '../../../components/Progress';
import {
completedChallengesIdsSelector,
isSignedInSelector
@@ -16,10 +26,10 @@ import {
} from '../redux/selectors';
import { apiLocation } from '../../../../config/env.json';
import { openModal, executeChallenge } from '../redux/actions';
import { saveChallenge } from '../../../redux/actions';
import Help from '../../../assets/icons/help';
import callGA from '../../../analytics/call-ga';
import { Share } from '../../../components/share';
import Reset from '../../../assets/icons/reset';
import { useSubmit } from '../utils/fetch-all-curriculum-data';
import './independent-lower-jaw.css';
@@ -51,13 +61,15 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = {
openHelpModal: () => openModal('help'),
openResetModal: () => openModal('reset'),
executeChallenge
executeChallenge,
saveChallenge
};
interface IndependentLowerJawProps {
openHelpModal: () => void;
openResetModal: () => void;
executeChallenge: () => void;
saveChallenge: () => void;
tests: Test[];
isSignedIn: boolean;
challengeMeta: ChallengeMeta;
@@ -69,6 +81,7 @@ export function IndependentLowerJaw({
openHelpModal,
openResetModal,
executeChallenge,
saveChallenge,
tests,
isSignedIn,
challengeMeta,
@@ -123,6 +136,7 @@ export function IndependentLowerJaw({
};
const isMacOS = navigator.userAgent.includes('Mac OS');
const showRevertButton = isSignedIn && challengeMeta.saveSubmissionToDB;
const checkButtonText = isMacOS
? t('buttons.command-enter')
: t('buttons.ctrl-enter');
@@ -138,14 +152,19 @@ export function IndependentLowerJaw({
className='hint-container'
data-playwright-test-label='independentLowerJaw-failing-hint'
>
<div className='hint-header'>
<FontAwesomeIcon icon={faLightbulb} />
<button
className={'tooltip'}
data-playwright-test-label='independentLowerJaw-hint-close-button'
onClick={() => setShowHint(false)}
aria-label={t('buttons.close')}
>
<FontAwesomeIcon icon={faClose} />
<span className='tooltiptext'> {t('buttons.close')}</span>
</button>
</div>
<div dangerouslySetInnerHTML={{ __html: hint }} />
<button
className={'tooltip'}
data-playwright-test-label='independentLowerJaw-hint-close-button'
onClick={() => setShowHint(false)}
>
×<span className='tooltiptext'> {t('buttons.close')}</span>
</button>
</div>
)}
{isChallengeComplete && showSubmissionHint && (
@@ -153,18 +172,37 @@ export function IndependentLowerJaw({
className='hint-container'
data-playwright-test-label='independentLowerJaw-submission-hint'
>
<div>
<p>{t('learn.congratulations-code-passes')}</p>
{isSignedIn && showShareButton && (
<div className='share-button-wrapper'>
<Share
superBlock={challengeMeta.superBlock}
block={challengeMeta.block}
minified={true}
/>
</div>
)}
{!isSignedIn && (
<div className='hint-header'>
<FontAwesomeIcon icon={faZap} />
<button
className={'tooltip'}
aria-label={t('buttons.close')}
data-playwright-test-label='independentLowerJaw-submission-hint-close-button'
onClick={() => setShowSubmissionHint(false)}
>
<FontAwesomeIcon icon={faClose} />
<span className='tooltiptext'> {t('buttons.close')}</span>
</button>
</div>
<b>{t('learn.congratulations-code-passes')}</b>
<div className='progress-bar-container'>
<Progress minified={true} />
</div>
{isSignedIn && showShareButton && (
<div
className='share-button-wrapper'
data-testid='share-button-wrapper'
>
<Share
superBlock={challengeMeta.superBlock}
block={challengeMeta.block}
minified={true}
/>
</div>
)}
{!isSignedIn && (
<>
<Spacer size='xxs' />
<a
href={`${apiLocation}/signin`}
className='btn-cta btn btn-block'
@@ -178,15 +216,8 @@ export function IndependentLowerJaw({
>
{t('learn.sign-in-save')}
</a>
)}
</div>
<button
className={'tooltip'}
data-playwright-test-label='independentLowerJaw-submission-hint-close-button'
onClick={() => setShowSubmissionHint(false)}
>
×<span className='tooltiptext'> {t('buttons.close')}</span>
</button>
</>
)}
</div>
)}
@@ -198,6 +229,7 @@ export function IndependentLowerJaw({
className={`${isSignedIn && 'btn-cta'} tooltip`}
id='independent-lower-jaw-submit-button'
data-playwright-test-label='independentLowerJaw-submit-button'
aria-label={t('buttons.submit-continue')}
onClick={() => submitChallenge()}
ref={submitButtonRef}
>
@@ -211,6 +243,7 @@ export function IndependentLowerJaw({
type='button'
className='btn-cta tooltip'
data-playwright-test-label='independentLowerJaw-check-button'
aria-label={t('buttons.check-code')}
onClick={handleCheckButtonClick}
>
{t('buttons.check-code')}
@@ -221,19 +254,46 @@ export function IndependentLowerJaw({
)}
</div>
<div className='action-row-right'>
<button
type='button'
className='icon-botton tooltip'
data-playwright-test-label='independentLowerJaw-reset-button'
onClick={openResetModal}
>
<Reset />
<span className='tooltiptext'> {t('buttons.reset')}</span>
</button>
{showRevertButton ? (
<>
<button
type='button'
className='icon-botton tooltip'
data-playwright-test-label='independentLowerJaw-save-button'
aria-label={t('buttons.save')}
onClick={() => saveChallenge()}
>
<FontAwesomeIcon icon={faSave} />
<span className='tooltiptext'> {t('buttons.save')}</span>
</button>
<button
type='button'
className='icon-botton tooltip'
data-playwright-test-label='independentLowerJaw-revert-button'
aria-label={t('buttons.revert')}
onClick={openResetModal}
>
<FontAwesomeIcon icon={faClockRotateLeft} />
<span className='tooltiptext'> {t('buttons.revert')}</span>
</button>
</>
) : (
<button
type='button'
className='icon-botton tooltip'
data-playwright-test-label='independentLowerJaw-reset-button'
aria-label={t('buttons.reset')}
onClick={openResetModal}
>
<FontAwesomeIcon icon={faRotateLeft} />
<span className='tooltiptext'> {t('buttons.reset')}</span>
</button>
)}
<button
type='button'
className='icon-botton tooltip'
data-playwright-test-label='independentLowerJaw-help-button'
aria-label={t('buttons.help')}
onClick={openHelpModal}
>
<Help />
@@ -34,7 +34,7 @@ interface SidePanelProps extends DispatchProps, StateProps {
hasDemo: boolean;
toolPanel: ReactNode;
tests: Test[];
showIndependentLowerJaw: boolean;
showSidePanelTests?: boolean;
}
export function SidePanel({
@@ -45,7 +45,7 @@ export function SidePanel({
toolPanel,
tests,
openModal,
showIndependentLowerJaw
showSidePanelTests
}: SidePanelProps): JSX.Element {
return (
<div
@@ -72,7 +72,7 @@ export function SidePanel({
</p>
)}{' '}
{challengeDescription}
{!showIndependentLowerJaw && (
{showSidePanelTests && (
<>
<Spacer size='m' />
{toolPanel}
@@ -7,8 +7,7 @@
}
.challenge-test-suite-heading {
font-size: 0.9em;
text-align: center;
font-size: 1em;
}
.sk-spinner {
@@ -25,6 +24,7 @@
width: 100%;
align-items: center;
line-height: 1.5;
padding: 6px;
}
.test-result:nth-child(odd) {
@@ -47,11 +47,9 @@
display: flex;
align-items: center;
justify-content: center;
min-width: 60px;
min-height: 60px;
}
.test-status-icon > svg {
width: 40px;
height: 40px;
width: 30px;
height: 30px;
}
+8 -18
View File
@@ -47,7 +47,7 @@ test('should render the modal content correctly', async ({ page }) => {
'/learn/responsive-web-design-v9/workshop-cat-photo-app/step-3'
);
await page.getByTestId('independentLowerJaw-reset-button').click();
await page.getByRole('button', { name: translations.buttons.reset }).click();
await expectToRenderResetModal(page);
@@ -93,14 +93,12 @@ test('User can reset challenge', async ({ page, isMobile, browserName }) => {
// are reset)
await page
.getByRole('button', {
// check-code works on all browsers because it does not include Command
// or Ctrl
name: translations.buttons['check-code']
})
.click();
// Reset the challenge
await page.getByTestId('independentLowerJaw-reset-button').click();
await page.getByRole('button', { name: translations.buttons.reset }).click();
await page
.getByRole('button', { name: translations.buttons['reset-lesson'] })
.click();
@@ -147,11 +145,9 @@ test.describe('When the user is not logged in', () => {
await focusEditor({ page, isMobile });
await getEditors(page).fill(challengeSolution);
const submitButton = page.getByRole('button', {
name: isMobile
? translations.buttons.run
: translations.buttons['run-test']
});
const submitButton = isMobile
? page.getByRole('button', { name: translations.buttons.run })
: page.getByRole('button', { name: translations.buttons['check-code'] });
await submitButton.click();
@@ -161,7 +157,7 @@ test.describe('When the user is not logged in', () => {
// Completion dialog shows up
await expect(
page.getByText(translations.buttons['go-to-next'])
page.getByText(translations.buttons['submit-continue'])
).toBeVisible();
// Close the dialog
@@ -170,11 +166,7 @@ test.describe('When the user is not logged in', () => {
.click();
await page
.getByRole('button', {
name: !isMobile
? translations.buttons['reset-lesson']
: translations.buttons.reset
})
.getByRole('button', { name: translations.buttons.reset })
.click();
await page
@@ -206,7 +198,7 @@ test('should close when the user clicks the close button', async ({ page }) => {
'/learn/responsive-web-design-v9/workshop-cat-photo-app/step-3'
);
await page.getByTestId('independentLowerJaw-reset-button').click();
await page.getByRole('button', { name: translations.buttons.reset }).click();
await expect(
page.getByRole('dialog', { name: translations.learn.reset })
@@ -242,13 +234,11 @@ test('User can reset on a multi-file project', async ({
await page.getByRole('button', { name: translations.buttons.revert }).click();
await expectToRenderResetModal(page);
await expect(
page.getByRole('button', {
name: translations.buttons['revert-to-saved-code']
})
).toBeVisible();
await page
.getByRole('button', {
name: translations.buttons['revert-to-saved-code']
+2 -5
View File
@@ -103,11 +103,8 @@ const completeChallenges = async ({
challenge.solution
);
await page.keyboard.press('ControlOrMeta+V');
await page.getByRole('button', { name: 'Run' }).click();
await expect(
page.getByRole('dialog').filter({ hasText: 'Basic Javascript' })
).toBeVisible(); // completion dialog
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button', { name: 'Check Your Code' }).click();
await page.getByRole('button', { name: 'Submit and continue' }).click();
}
};
+88 -7
View File
@@ -1,9 +1,16 @@
import { test, expect } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
test.describe('help-button tests for a page with three links (hint, help and video)', () => {
test.describe('Mobile help-button tests for a page with three links (hint, help and video)', () => {
test.use({
viewport: { width: 393, height: 851 },
isMobile: true
});
test('should render the button, menu and the three links when video is available', async ({
page
page,
isMobile
}) => {
test.skip(!isMobile, 'Help dropdown only available on mobile');
// visit the page with the video link
await page.goto(
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements'
@@ -20,11 +27,17 @@ test.describe('help-button tests for a page with three links (hint, help and vid
});
});
test.describe('help-button tests for a page with two links when video is not available', () => {
test.describe('Mobile help-button tests for a page with two links when video is not available', () => {
test.use({
viewport: { width: 393, height: 851 },
isMobile: true
});
test('should render the button, menu and the two links when video is not available', async ({
page
page,
isMobile
}) => {
// visit the page without the video link
test.skip(!isMobile, 'Help dropdown only available on mobile');
await page.goto(
'/learn/front-end-development-libraries/bootstrap/apply-the-default-bootstrap-button-style'
);
@@ -81,10 +94,78 @@ test.describe('Desktop help-button tests for a page with a reset and help button
'learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-3'
);
await expect(
page.getByTestId('independentLowerJaw-reset-button')
page.getByRole('button', { name: translations.buttons.reset })
).toBeVisible();
await expect(
page.getByTestId('independentLowerJaw-help-button')
page.getByRole('button', { name: translations.buttons.help })
).toBeVisible();
});
test('should open help modal with video button when video is available', async ({
page
}) => {
// visit the page with the video link
await page.goto(
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements'
);
// Click the help button in independent lower jaw
const helpButton = page.getByRole('button', {
name: translations.buttons.help
});
await expect(helpButton).toBeVisible();
await helpButton.click();
// Help modal should open
await expect(
page.getByRole('dialog', { name: translations.buttons['get-help'] })
).toBeVisible();
// Video button should be present in the modal
await expect(
page.getByRole('button', { name: translations.buttons['watch-video'] })
).toBeVisible();
// Other help options should be present
await expect(
page.getByRole('link', { name: translations.buttons['get-hint'] })
).toBeVisible();
await expect(
page.getByRole('button', { name: translations.buttons['create-post'] })
).toBeVisible();
});
test('should open help modal without video button when video is not available', async ({
page
}) => {
// visit the page without the video link
await page.goto(
'/learn/front-end-development-libraries/bootstrap/apply-the-default-bootstrap-button-style'
);
// Click the help button in independent lower jaw
const helpButton = page.getByRole('button', {
name: translations.buttons.help
});
await expect(helpButton).toBeVisible();
await helpButton.click();
// Help modal should open
await expect(
page.getByRole('dialog', { name: translations.buttons['get-help'] })
).toBeVisible();
// Video button should NOT be present in the modal
await expect(
page.getByRole('button', { name: translations.buttons['watch-video'] })
).toBeHidden();
// Other help options should be present
await expect(
page.getByRole('link', { name: translations.buttons['get-hint'] })
).toBeVisible();
await expect(
page.getByRole('button', { name: translations.buttons['create-post'] })
).toBeVisible();
});
});
+6 -6
View File
@@ -14,7 +14,7 @@ test.describe('Help Modal component', () => {
.click();
await expect(
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
page.getByRole('dialog', { name: translations.buttons['get-help'] })
).toBeVisible();
await expect(
page.getByText(
@@ -52,7 +52,7 @@ test.describe('Help Modal component', () => {
.click();
await expect(
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
page.getByRole('dialog', { name: translations.buttons['get-help'] })
).toBeVisible();
const rsaCheckbox = page.getByRole('checkbox', {
@@ -102,7 +102,7 @@ test.describe('Help Modal component', () => {
.click();
await expect(
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
page.getByRole('dialog', { name: translations.buttons['get-help'] })
).toBeVisible();
const rsaCheckbox = page.getByRole('checkbox', {
@@ -143,7 +143,7 @@ test.describe('Help Modal component', () => {
.click();
await expect(
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
page.getByRole('dialog', { name: translations.buttons['get-help'] })
).toBeVisible();
const rsaCheckbox = page.getByRole('checkbox', {
@@ -189,7 +189,7 @@ test.describe('Help Modal component', () => {
.click();
const dialog = page.getByRole('dialog', {
name: translations.buttons['ask-for-help']
name: translations.buttons['get-help']
});
await expect(dialog).toBeVisible();
@@ -207,7 +207,7 @@ test.describe('Help Modal component', () => {
.click();
const dialog = page.getByRole('dialog', {
name: translations.buttons['ask-for-help']
name: translations.buttons['get-help']
});
await expect(dialog).toBeVisible();
+1 -1
View File
@@ -43,7 +43,7 @@ test('Correct Ask for help button', async ({ page }) => {
await checkAnswerButton.click();
await expect(
page.getByRole('heading', {
name: translations.buttons['ask-for-help'],
name: translations.buttons['get-help'],
exact: true
})
).toBeVisible();
+3 -3
View File
@@ -1,6 +1,8 @@
import { execSync } from 'child_process';
import { test, expect } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
import { clearEditor, focusEditor } from './utils/editor';
test.describe('multifileCertProjects', () => {
test.beforeEach(async ({ page }) => {
execSync('node ../tools/scripts/seed/seed-demo-user --certified-user');
@@ -25,9 +27,7 @@ test.describe('multifileCertProjects', () => {
await page.keyboard.type('save1text');
await expect(page.getByText('save1text')).toBeVisible();
await page
.getByRole('button', { name: !isMobile ? 'Save your Code' : 'Save' })
.click();
await page.getByRole('button', { name: translations.buttons.save }).click();
await expect(page.getByTestId('flash-message')).toContainText(success);
+8 -2
View File
@@ -55,7 +55,11 @@ const runChallengeTest = async (page: Page, isMobile: boolean) => {
await page.getByRole('tab', { name: 'Console' }).click();
await page.getByText('Run').click();
} else {
await page.getByText('Run the Tests (Ctrl + Enter)').click();
await page
.getByRole('button', {
name: translations.buttons['check-code']
})
.click();
}
};
@@ -95,7 +99,9 @@ test.describe('For classic challenges', () => {
text: '<h1>Hello World</h1>'
});
await runChallengeTest(page, isMobile);
await closeButton.click();
if (isMobile) {
await closeButton.click();
}
await expect(
page.getByRole('region', {
+21 -14
View File
@@ -1,11 +1,8 @@
import { expect, test } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
import { clearEditor, focusEditor } from './utils/editor';
test.describe('Progress bar component in editor', () => {
// progress bar shows up for the defeult lower jaw that is only displayed on mobile.
test.use({
viewport: { width: 390, height: 844 }
});
test('Should appear with the correct content after the user has submitted their code', async ({
page,
isMobile,
@@ -24,20 +21,24 @@ test.describe('Progress bar component in editor', () => {
'<html><body><h1>CatPhotoApp</h1><h2>Cat Photos</h2><p>Everyone loves cute cats online!</p></body></html>'
);
await page.getByRole('button', { name: 'Check Your Code' }).click();
await page
.getByRole('button', { name: translations.buttons['check-code'] })
.click();
const progressBarContainer = page.getByTestId('progress-bar-container');
await expect(progressBarContainer).toContainText(
'Learn HTML by Building a Cat Photo App'
);
await expect(progressBarContainer).toContainText(/\d% complete/);
await page
.getByRole('button', { name: 'Submit and go to next challenge' })
.click();
await page.getByRole('button', { name: 'Submit and continue' }).click();
});
});
test.describe('Progress bar component in modal', () => {
test.use({
viewport: { width: 393, height: 851 },
isMobile: true
});
test('should appear in the completion modal after user has submitted their code', async ({
page,
isMobile,
@@ -51,12 +52,18 @@ test.describe('Progress bar component in modal', () => {
await page.keyboard.insertText('var myName;');
await page
.getByRole('button', {
name: 'Run',
exact: false
})
.click();
if (isMobile) {
await page
.getByRole('button', {
name: 'Run',
exact: false
})
.click();
} else {
await page
.getByRole('button', { name: translations.buttons['check-code'] })
.click();
}
await expect(page.locator('.completion-block-meta')).toContainText(
/\d% complete/
+7 -2
View File
@@ -158,10 +158,15 @@ test.describe('JavaScript projects can be submitted and then viewed in /settings
await pasteContent(page);
await page.getByRole('button', { name: 'Run' }).click();
await page
.getByRole('button', { name: translations.buttons['check-code'] })
.click();
await page
.getByRole('button', { name: 'Go to next challenge', exact: false })
.getByRole('button', {
name: translations.buttons['submit-continue'],
exact: false
})
.click();
// Submit the rest with the API.
+1 -3
View File
@@ -9,10 +9,8 @@ test.beforeEach(async ({ page }) => {
test.describe('Challenge Side Panel Component', () => {
test('should render correctly', async ({ page, isMobile }) => {
const toolPanelItem = page.getByText(translations.buttons['get-help']);
if (isMobile) {
await expect(toolPanelItem).not.toBeVisible();
} else {
const toolPanelItem = page.getByText(translations.buttons['get-help']);
await expect(toolPanelItem).toBeVisible();
}
await expect(page.getByTestId('challenge-title')).toBeVisible();
+3 -1
View File
@@ -5,7 +5,9 @@ const runChallengeTest = async (page: Page, isMobile: boolean) => {
if (isMobile) {
await page.getByText('Run').click();
} else {
await page.getByText('Run the Tests (Ctrl + Enter)').click();
await page
.getByRole('button', { name: translations.buttons['check-code'] })
.click();
}
};
+14 -9
View File
@@ -11,19 +11,22 @@ test.describe('Tool Panel', () => {
page,
isMobile
}) => {
await page
.getByRole('button', {
name: 'Run',
exact: false
})
.click();
if (isMobile) {
await page
.getByRole('button', {
name: 'Run',
exact: false
})
.click();
await page
.getByRole('tab', {
name: 'Console'
})
.click();
} else {
await page
.getByRole('button', { name: translations.buttons['check-code'] })
.click();
}
await expect(page.getByTestId('output-text')).toContainText(
@@ -41,7 +44,7 @@ test.describe('Tool Panel', () => {
.click();
} else {
await page
.getByRole('button', { name: translations.buttons['reset-lesson'] })
.getByRole('button', { name: translations.buttons.reset })
.click();
}
await expect(
@@ -50,8 +53,10 @@ test.describe('Tool Panel', () => {
});
test('should display list with expected links after clicking "Get Help"', async ({
page
page,
isMobile
}) => {
test.skip(!isMobile, 'Help dropdown only available on mobile');
const expectedHelpLinks = [
`${translations.buttons['get-hint']} , ${translations.aria['opens-new-window']}`,
translations.buttons['watch-video'],
+56 -6
View File
@@ -4,13 +4,18 @@ import translations from '../client/i18n/locales/english/translations.json';
const currentUrlPath =
'/learn/responsive-web-design/responsive-web-design-principles/create-a-media-query';
test.beforeEach(async ({ page }) => {
await page.goto(currentUrlPath);
await page.getByTestId('get-help-dropdown').click();
await page.getByTestId('watch-a-video').click();
});
test.describe('Exit Video Modal E2E Test Suite - Mobile', () => {
test.use({
viewport: { width: 393, height: 851 },
isMobile: true
});
test.beforeEach(async ({ page }) => {
await page.goto(currentUrlPath);
await page.getByTestId('get-help-dropdown').click();
await page.getByTestId('watch-a-video').click();
});
test.describe('Exit Video Modal E2E Test Suite', () => {
test('Verifies the Correct Rendering of the Video Modal', async ({
page
}) => {
@@ -44,3 +49,48 @@ test.describe('Exit Video Modal E2E Test Suite', () => {
await expect(dialog).not.toBeVisible();
});
});
test.describe('Exit Video Modal E2E Test Suite - Desktop', () => {
test.beforeEach(async ({ page }) => {
await page.goto(currentUrlPath);
// Open help modal via independent lower jaw
await page.getByRole('button', { name: translations.buttons.help }).click();
// Click watch video link from help modal
await page.getByTestId('watch-a-video-modal-button').click();
});
test('Verifies the Correct Rendering of the Video Modal on Desktop', async ({
page
}) => {
const dialog = page.getByRole('dialog', {
name: translations.buttons['watch-video']
});
await expect(dialog).toBeVisible();
await expect(
page.getByTestId('video-modal-video-player-iframe')
).toBeVisible();
await expect(
page.getByText(translations.learn['scrimba-tip'])
).toBeVisible();
await expect(
dialog.getByRole('button', { name: translations.buttons.close })
).toBeVisible();
});
test('Closes the Video Modal When the User clicks on exit button on Desktop', async ({
page
}) => {
const dialog = page.getByRole('dialog', {
name: translations.buttons['watch-video']
});
await expect(dialog).toBeVisible();
await dialog
.getByRole('button', { name: translations.buttons.close })
.click();
await expect(dialog).not.toBeVisible();
});
});