feat:(client): show-workshop-independent-lower-jaw (#64137)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2026-01-08 17:51:57 +03:00
committed by GitHub
parent 87cf2f2633
commit c3f0473cd0
13 changed files with 292 additions and 36 deletions

View File

@@ -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;

View File

@@ -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 })}

View File

@@ -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') {

View File

@@ -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 {

View File

@@ -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 />

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.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 })

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View 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);
});
});

View File

@@ -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}'

View File

@@ -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

View File

@@ -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,

View File

@@ -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');
});