mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-02-12 18:00:39 -05:00
feat:(client): show-workshop-independent-lower-jaw (#64137)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -14,6 +14,22 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#learn-app-wrapper .editor-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#learn-app-wrapper .editor-pane-code {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
#learn-app-wrapper .editor-pane > .independent-lower-jaw {
|
||||
flex: 0 0 auto;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
#learn-app-wrapper .reflex-container.vertical {
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -314,9 +314,14 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
name='editorPane'
|
||||
{...resizeProps}
|
||||
data-playwright-test-label='editor-pane'
|
||||
className='editor-pane'
|
||||
>
|
||||
{!isEmpty(challengeFiles) && (
|
||||
<ReflexContainer key='codePane' orientation='horizontal'>
|
||||
<ReflexContainer
|
||||
key='codePane'
|
||||
orientation='horizontal'
|
||||
className='editor-pane-code'
|
||||
>
|
||||
<ReflexElement
|
||||
name='codePane'
|
||||
{...(displayEditorConsole && { flex: codePane.flex })}
|
||||
|
||||
@@ -314,13 +314,9 @@ function ShowClassic({
|
||||
|
||||
// AB testing Pre-fetch in the Spanish locale
|
||||
const isPreFetchEnabled = useFeature('prefetch_ab_test').on;
|
||||
const isIndependentLowerJawEnabled = useFeature('independent-lower-jaw').on;
|
||||
|
||||
// Independent lower jaw is only enabled for the urriculum outline workshop
|
||||
const showIndependentLowerJaw =
|
||||
block === 'workshop-curriculum-outline' &&
|
||||
isIndependentLowerJawEnabled &&
|
||||
!isMobile;
|
||||
// Independent lower jaw is only enabled for desktop workshops.
|
||||
const showIndependentLowerJaw = hasEditableBoundaries && !isMobile;
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreFetchEnabled && envData.clientLocale === 'espanol') {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
.independent-lower-jaw {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.independent-lower-jaw .hint-container {
|
||||
@@ -13,7 +11,24 @@
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
margin: 12px;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
width: 95%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--background-secondary);
|
||||
opacity: 0;
|
||||
animation: jaw-hint-fade-in 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes jaw-hint-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.independent-lower-jaw .btn-cta {
|
||||
|
||||
@@ -51,6 +51,10 @@ export function IndependentLowerJaw({
|
||||
const hint = firstFailedTest?.message;
|
||||
const [showHint, setShowHint] = React.useState(false);
|
||||
const [showSubmissionHint, setShowSubmissionHint] = React.useState(true);
|
||||
const signInLinkRef = React.useRef<HTMLAnchorElement>(null);
|
||||
const submitButtonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [wasCheckButtonClicked, setWasCheckButtonClicked] =
|
||||
React.useState(false);
|
||||
|
||||
const isChallengeComplete = tests.every(test => test.pass);
|
||||
|
||||
@@ -58,27 +62,60 @@ export function IndependentLowerJaw({
|
||||
setShowHint(!!hint);
|
||||
}, [hint]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isChallengeComplete || !wasCheckButtonClicked) return;
|
||||
|
||||
const focusTarget = isSignedIn
|
||||
? submitButtonRef.current
|
||||
: signInLinkRef.current;
|
||||
focusTarget?.focus();
|
||||
setWasCheckButtonClicked(false);
|
||||
}, [isChallengeComplete, isSignedIn, wasCheckButtonClicked]);
|
||||
|
||||
const handleCheckButtonClick = () => {
|
||||
setWasCheckButtonClicked(true);
|
||||
executeChallenge();
|
||||
};
|
||||
|
||||
const isMacOS = navigator.userAgent.includes('Mac OS');
|
||||
const checkButtonText = isMacOS ? t('command-enter') : t('ctrl-enter');
|
||||
const checkButtonText = isMacOS
|
||||
? t('buttons.command-enter')
|
||||
: t('buttons.ctrl-enter');
|
||||
|
||||
return (
|
||||
<div className='independent-lower-jaw' tabIndex={-1}>
|
||||
<div
|
||||
className='independent-lower-jaw'
|
||||
data-playwright-test-label='independentLowerJaw-container'
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showHint && hint && (
|
||||
<div className='hint-container'>
|
||||
<div
|
||||
className='hint-container'
|
||||
data-playwright-test-label='independentLowerJaw-failing-hint'
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: hint }} />
|
||||
<button className={'tooltip'} onClick={() => setShowHint(false)}>
|
||||
<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 && (
|
||||
<div className='hint-container'>
|
||||
<div
|
||||
className='hint-container'
|
||||
data-playwright-test-label='independentLowerJaw-submission-hint'
|
||||
>
|
||||
<div>
|
||||
<p>{t('learn.congratulations-code-passes')}</p>
|
||||
{!isSignedIn && (
|
||||
<a
|
||||
href={`${apiLocation}/signin`}
|
||||
className='btn-cta btn btn-block'
|
||||
data-playwright-test-label='independentLowerJaw-signin-link'
|
||||
ref={signInLinkRef}
|
||||
onClick={() => {
|
||||
callGA({
|
||||
event: 'sign_in'
|
||||
@@ -91,6 +128,7 @@ export function IndependentLowerJaw({
|
||||
</div>
|
||||
<button
|
||||
className={'tooltip'}
|
||||
data-playwright-test-label='independentLowerJaw-submission-hint-close-button'
|
||||
onClick={() => setShowSubmissionHint(false)}
|
||||
>
|
||||
×<span className='tooltiptext'> {t('buttons.close')}</span>
|
||||
@@ -104,7 +142,10 @@ export function IndependentLowerJaw({
|
||||
<Button
|
||||
block
|
||||
className={`${isSignedIn && 'btn-cta'} tooltip`}
|
||||
id='independent-lower-jaw-submit-button'
|
||||
data-playwright-test-label='independentLowerJaw-submit-button'
|
||||
onClick={() => submitChallenge()}
|
||||
ref={submitButtonRef}
|
||||
>
|
||||
{t('buttons.submit-continue')}
|
||||
<span className='tooltiptext left-tooltip '>
|
||||
@@ -115,7 +156,8 @@ export function IndependentLowerJaw({
|
||||
<button
|
||||
type='button'
|
||||
className='btn-cta tooltip'
|
||||
onClick={() => executeChallenge()}
|
||||
data-playwright-test-label='independentLowerJaw-check-button'
|
||||
onClick={handleCheckButtonClick}
|
||||
>
|
||||
{t('buttons.check-code')}
|
||||
<span className='tooltiptext left-tooltip '>
|
||||
@@ -128,6 +170,7 @@ export function IndependentLowerJaw({
|
||||
<button
|
||||
type='button'
|
||||
className='icon-botton tooltip'
|
||||
data-playwright-test-label='independentLowerJaw-reset-button'
|
||||
onClick={openResetModal}
|
||||
>
|
||||
<Reset />
|
||||
@@ -136,6 +179,7 @@ export function IndependentLowerJaw({
|
||||
<button
|
||||
type='button'
|
||||
className='icon-botton tooltip'
|
||||
data-playwright-test-label='independentLowerJaw-help-button'
|
||||
onClick={openHelpModal}
|
||||
>
|
||||
<Help />
|
||||
|
||||
@@ -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.getByRole('button', { name: translations.buttons.reset }).click();
|
||||
await page.getByTestId('independentLowerJaw-reset-button').click();
|
||||
|
||||
await expectToRenderResetModal(page);
|
||||
|
||||
@@ -99,12 +99,8 @@ test('User can reset challenge', async ({ page, isMobile, browserName }) => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText(translations.learn['sorry-keep-trying'])
|
||||
).toBeVisible();
|
||||
|
||||
// Reset the challenge
|
||||
await page.getByTestId('lowerJaw-reset-button').click();
|
||||
await page.getByTestId('independentLowerJaw-reset-button').click();
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons['reset-lesson'] })
|
||||
.click();
|
||||
@@ -210,7 +206,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.getByRole('button', { name: translations.buttons.reset }).click();
|
||||
await page.getByTestId('independentLowerJaw-reset-button').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog', { name: translations.learn.reset })
|
||||
|
||||
@@ -41,7 +41,7 @@ test.describe('when reloading the page', () => {
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(
|
||||
page.getByText(translations.learn.congratulations)
|
||||
page.getByText(translations.learn['congratulations-code-passes'])
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,12 @@ test.describe('help-button tests for a page with two links when video is not ava
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('help-button tests for a page with a reset and help button', () => {
|
||||
test.describe('Mobile help-button tests for a page with a reset and help button', () => {
|
||||
// Test the lower jaw on mobile viewport only
|
||||
test.use({
|
||||
viewport: { width: 393, height: 851 },
|
||||
isMobile: true
|
||||
});
|
||||
test('should not be present before the user checks their code three times', async ({
|
||||
page
|
||||
}) => {
|
||||
@@ -69,3 +74,17 @@ test.describe('help-button tests for a page with a reset and help button', () =>
|
||||
await expect(helpIconGroup).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Desktop help-button tests for a page with a reset and help button', () => {
|
||||
test('should always be shown', async ({ page }) => {
|
||||
await page.goto(
|
||||
'learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-3'
|
||||
);
|
||||
await expect(
|
||||
page.getByTestId('independentLowerJaw-reset-button')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('independentLowerJaw-help-button')
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
151
e2e/independent-lower-jaw.spec.ts
Normal file
151
e2e/independent-lower-jaw.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { clearEditor, focusEditor, getEditors } from './utils/editor';
|
||||
|
||||
const workshopChallengeUrl =
|
||||
'/learn/responsive-web-design-v9/workshop-cafe-menu/step-2';
|
||||
const penguinChallengeUrl =
|
||||
'/learn/2022/responsive-web-design/learn-css-transforms-by-building-a-penguin/step-4';
|
||||
|
||||
const workshopPassingSolution = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Cafe Menu</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>CAMPER CAFE</h1>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
test.use({
|
||||
viewport: { width: 1080, height: 720 }
|
||||
});
|
||||
|
||||
test('Clicking "Check Your Code" reveals failing feedback', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(workshopChallengeUrl);
|
||||
|
||||
await page.getByTestId('independentLowerJaw-check-button').click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('independentLowerJaw-failing-hint')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Reset button opens and closes the reset modal', async ({ page }) => {
|
||||
await page.goto(workshopChallengeUrl);
|
||||
|
||||
await page.getByTestId('independentLowerJaw-reset-button').click();
|
||||
|
||||
const resetModal = page.getByRole('dialog', { name: 'Reset this lesson?' });
|
||||
await expect(resetModal).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /close/i }).click();
|
||||
await expect(resetModal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Checks hotkeys when instruction is focused', async ({
|
||||
page,
|
||||
browserName
|
||||
}) => {
|
||||
await page.goto(workshopChallengeUrl);
|
||||
|
||||
const editor = getEditors(page);
|
||||
const description = page.locator('#description');
|
||||
await focusEditor({ page, isMobile: false });
|
||||
await clearEditor({ page, browserName, isMobile: false });
|
||||
await editor.fill(workshopPassingSolution);
|
||||
await description.click();
|
||||
|
||||
if (browserName === 'webkit') {
|
||||
await page.keyboard.press('Meta+Enter');
|
||||
} else {
|
||||
await page.keyboard.press('Control+Enter');
|
||||
}
|
||||
|
||||
await expect(
|
||||
page.getByTestId('independentLowerJaw-check-button')
|
||||
).not.toBeFocused();
|
||||
});
|
||||
|
||||
test('Hint text should not contain placeholders `fcc-expected`', async ({
|
||||
page,
|
||||
browserName,
|
||||
isMobile
|
||||
}) => {
|
||||
await page.goto(penguinChallengeUrl);
|
||||
|
||||
const editor = getEditors(page);
|
||||
await focusEditor({ page, isMobile });
|
||||
await clearEditor({ page, browserName, isMobile });
|
||||
await editor.fill(
|
||||
'body{background:linear-gradient(45deg, rgb(118, 201, 255), rgb(247, 255, 222));margin:0;padding:0;width:5%;height:100vh}'
|
||||
);
|
||||
|
||||
await page.getByTestId('independentLowerJaw-check-button').click();
|
||||
|
||||
const hintDescription = page.getByTestId('independentLowerJaw-failing-hint');
|
||||
await expect(hintDescription).toContainText(
|
||||
'You should give body a width of 100%, but found 5%',
|
||||
{ useInnerText: true }
|
||||
);
|
||||
});
|
||||
|
||||
test.describe('Unauthenticated user', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('Focuses on the sign in button when unauthenticated user completes a step', async ({
|
||||
page,
|
||||
browserName,
|
||||
isMobile
|
||||
}) => {
|
||||
await page.goto(workshopChallengeUrl);
|
||||
|
||||
const editor = getEditors(page);
|
||||
await focusEditor({ page, isMobile });
|
||||
await clearEditor({ page, browserName, isMobile });
|
||||
await editor.fill(workshopPassingSolution);
|
||||
|
||||
await page.getByTestId('independentLowerJaw-check-button').click();
|
||||
|
||||
const signInLink = page.getByTestId('independentLowerJaw-signin-link');
|
||||
await expect(
|
||||
page.getByTestId('independentLowerJaw-submit-button')
|
||||
).toBeVisible();
|
||||
await expect(signInLink).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('independentLowerJaw-check-button')
|
||||
).toHaveCount(0);
|
||||
await expect(signInLink).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Authenticated user', () => {
|
||||
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||
|
||||
test('Focuses on the submit button when authenticated user completes a step', async ({
|
||||
page,
|
||||
browserName,
|
||||
isMobile
|
||||
}) => {
|
||||
await page.goto(workshopChallengeUrl);
|
||||
|
||||
const editor = getEditors(page);
|
||||
await focusEditor({ page, isMobile });
|
||||
await clearEditor({ page, browserName, isMobile });
|
||||
await editor.fill(workshopPassingSolution);
|
||||
|
||||
await page.getByTestId('independentLowerJaw-check-button').click();
|
||||
|
||||
const submitButton = page.getByTestId('independentLowerJaw-submit-button');
|
||||
await expect(submitButton).toBeVisible();
|
||||
await expect(submitButton).toContainText('Submit and continue');
|
||||
await expect(submitButton).toBeFocused();
|
||||
await expect(
|
||||
page.getByTestId('independentLowerJaw-signin-link')
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,12 @@ import { test, expect } from '@playwright/test';
|
||||
import { clearEditor, focusEditor, getEditors } from './utils/editor';
|
||||
import { signout } from './utils/logout';
|
||||
|
||||
// Test the lower jaw on mobile viewport only
|
||||
test.use({
|
||||
viewport: { width: 393, height: 851 },
|
||||
isMobile: true
|
||||
});
|
||||
|
||||
test('Check the initial states of submit button and "check your code" button', async ({
|
||||
page
|
||||
}) => {
|
||||
@@ -97,7 +103,7 @@ test('Focuses on the submit button after tests passed', async ({
|
||||
name: 'Submit and go to next challenge'
|
||||
});
|
||||
await focusEditor({ page, isMobile });
|
||||
await clearEditor({ page, browserName });
|
||||
await clearEditor({ page, browserName, isMobile });
|
||||
|
||||
await editor.fill(
|
||||
'<h2>Cat Photos</h2>\n<p>Everyone loves cute cats online!</p>'
|
||||
@@ -123,7 +129,7 @@ test('Prompts unauthenticated user to sign in to save progress', async ({
|
||||
name: 'Sign in to save your progress'
|
||||
});
|
||||
await focusEditor({ page, isMobile });
|
||||
await clearEditor({ page, browserName });
|
||||
await clearEditor({ page, browserName, isMobile });
|
||||
|
||||
await editor.fill(
|
||||
'<h2>Cat Photos</h2>\n<p>Everyone loves cute cats online!</p>'
|
||||
@@ -185,9 +191,8 @@ test('should display the text of submit and go to next challenge button accordin
|
||||
);
|
||||
const editor = getEditors(page);
|
||||
const checkButton = page.getByRole('button', { name: 'Check Your Code' });
|
||||
|
||||
await focusEditor({ page, isMobile });
|
||||
await clearEditor({ page, browserName });
|
||||
await clearEditor({ page, browserName, isMobile });
|
||||
|
||||
await editor.fill(
|
||||
'<h2>Cat Photos</h2>\n<p>Everyone loves cute cats online!</p>'
|
||||
@@ -228,7 +233,7 @@ test('Hint text should not contain placeholders `fcc-expected`', async ({
|
||||
const editor = getEditors(page);
|
||||
const checkButton = page.getByRole('button', { name: 'Check Your Code' });
|
||||
await focusEditor({ page, isMobile });
|
||||
await clearEditor({ page, browserName });
|
||||
await clearEditor({ page, browserName, isMobile });
|
||||
|
||||
await editor.fill(
|
||||
'body{background:linear-gradient(45deg, rgb(118, 201, 255), rgb(247, 255, 222));margin:0;padding:0;width:5%;height:100vh}'
|
||||
|
||||
@@ -104,7 +104,7 @@ test.describe('MultifileEditor Component', () => {
|
||||
await page.keyboard.press('Control+Enter');
|
||||
|
||||
const submitButton = page.getByRole('button', {
|
||||
name: 'Submit and go to next challenge'
|
||||
name: 'Submit and Continue'
|
||||
});
|
||||
|
||||
// Mobile screen shifts submit button out of view and Playwright fails at scrolling with multiple editors open
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { clearEditor, focusEditor } from './utils/editor';
|
||||
|
||||
test.describe('Progress bar component', () => {
|
||||
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,
|
||||
@@ -31,7 +35,9 @@ test.describe('Progress bar component', () => {
|
||||
.getByRole('button', { name: 'Submit and go to next challenge' })
|
||||
.click();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Progress bar component in modal', () => {
|
||||
test('should appear in the completion modal after user has submitted their code', async ({
|
||||
page,
|
||||
isMobile,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Editor scrollbar width', () => {
|
||||
test.describe('Editor scrollbar width on mobile', () => {
|
||||
test.use({
|
||||
viewport: { width: 393, height: 851 }
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user