mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): add action row with interactive editor toggle to lectures (#62928)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -523,7 +523,8 @@
|
||||
"instructions": "Instructions",
|
||||
"notes": "Notes",
|
||||
"preview": "Preview",
|
||||
"editor": "Editor"
|
||||
"editor": "Editor",
|
||||
"interactive-editor": "Interactive Editor"
|
||||
},
|
||||
"editor-alerts": {
|
||||
"tab-trapped": "Pressing tab will now insert the tab character",
|
||||
@@ -952,7 +953,8 @@
|
||||
"editor-a11y-on-macos": "{{editorName}} editor content. Accessibility mode set to 'on'. Press Command+E to disable or press Option+F1 for more options.",
|
||||
"editor-a11y-on-non-macos": "{{editorName}} editor content. Accessibility mode set to 'on'. Press Ctrl+E to disable or press Alt+F1 for more options.",
|
||||
"terminal-output": "Terminal output",
|
||||
"not-available": "Not available"
|
||||
"not-available": "Not available",
|
||||
"interactive-editor-desc": "Turn static code examples into interactive editors. This allows you to edit and run the code directly on the page."
|
||||
},
|
||||
"flash": {
|
||||
"no-email-in-userinfo": "We could not retrieve an email from your chosen provider. Please try another provider or use the 'Continue with Email' option.",
|
||||
|
||||
@@ -10,5 +10,5 @@ div.flash-message {
|
||||
padding-bottom: 3px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 150;
|
||||
z-index: var(--z-index-flash);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
z-index: var(--z-index-site-header);
|
||||
}
|
||||
|
||||
@media (min-width: 980px) {
|
||||
|
||||
@@ -139,6 +139,14 @@ hr {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.default-layout:has(.breadcrumbs-demo) #learn-app-wrapper {
|
||||
padding-top: var(--breadcrumbs-height);
|
||||
}
|
||||
|
||||
.default-layout:has(.action-row) #learn-app-wrapper {
|
||||
padding-top: calc(var(--breadcrumbs-height) + var(--action-row-height));
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--secondary-color);
|
||||
font-weight: 700;
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
--header-sub-element-size: 45px;
|
||||
--header-height: 38px;
|
||||
--breadcrumbs-height: 44px;
|
||||
--action-row-height: 64px;
|
||||
--z-index-breadcrumbs: 100;
|
||||
--z-index-flash: 150;
|
||||
--z-index-site-header: 200;
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,12 @@ type ParagraphNodule = {
|
||||
|
||||
type InteractiveEditorNodule = {
|
||||
type: 'interactiveEditor';
|
||||
data: { ext: Ext; name: string; contents: string }[];
|
||||
data: {
|
||||
ext: Ext;
|
||||
name: string;
|
||||
contents: string;
|
||||
contentsHtml: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type ChallengeNode = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import store from 'store';
|
||||
import { DailyCodingChallengeLanguages } from '../../../redux/prop-types';
|
||||
import EditorTabs from './editor-tabs';
|
||||
|
||||
interface ActionRowProps {
|
||||
interface ClassicLayoutProps {
|
||||
dailyCodingChallengeLanguage: DailyCodingChallengeLanguages;
|
||||
hasNotes: boolean;
|
||||
hasPreview: boolean;
|
||||
@@ -21,24 +21,58 @@ interface ActionRowProps {
|
||||
showPreviewPane: boolean;
|
||||
showPreviewPortal: boolean;
|
||||
togglePane: (pane: string) => void;
|
||||
hasInteractiveEditor?: never;
|
||||
}
|
||||
|
||||
const ActionRow = ({
|
||||
hasPreview,
|
||||
hasNotes,
|
||||
togglePane,
|
||||
showNotes,
|
||||
showPreviewPane,
|
||||
showPreviewPortal,
|
||||
showConsole,
|
||||
showInstructions,
|
||||
areInstructionsDisplayable,
|
||||
isDailyCodingChallenge,
|
||||
dailyCodingChallengeLanguage,
|
||||
setDailyCodingChallengeLanguage
|
||||
}: ActionRowProps): JSX.Element => {
|
||||
interface InteractiveEditorProps {
|
||||
hasInteractiveEditor: true;
|
||||
showInteractiveEditor: boolean;
|
||||
toggleInteractiveEditor: () => void;
|
||||
}
|
||||
|
||||
type ActionRowProps = ClassicLayoutProps | InteractiveEditorProps;
|
||||
|
||||
const ActionRow = (props: ActionRowProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (props.hasInteractiveEditor) {
|
||||
const { toggleInteractiveEditor, showInteractiveEditor } = props;
|
||||
|
||||
return (
|
||||
<div className='action-row'>
|
||||
<div className='tabs-row'>
|
||||
<div className='tabs-row-right'>
|
||||
<button
|
||||
aria-expanded={!!showInteractiveEditor}
|
||||
aria-describedby='interactive-editor-desc'
|
||||
onClick={toggleInteractiveEditor}
|
||||
>
|
||||
{t('learn.editor-tabs.interactive-editor')}
|
||||
</button>
|
||||
<span id='interactive-editor-desc' className='sr-only'>
|
||||
{t('aria.interactive-editor-desc')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
togglePane,
|
||||
hasPreview,
|
||||
hasNotes,
|
||||
areInstructionsDisplayable,
|
||||
showConsole,
|
||||
showNotes,
|
||||
showInstructions,
|
||||
showPreviewPane,
|
||||
showPreviewPortal,
|
||||
isDailyCodingChallenge,
|
||||
dailyCodingChallengeLanguage,
|
||||
setDailyCodingChallengeLanguage
|
||||
} = props;
|
||||
|
||||
// sets screen reader text for the two preview buttons
|
||||
function getPreviewBtnsSrText() {
|
||||
// no preview open
|
||||
|
||||
@@ -18,8 +18,15 @@
|
||||
}
|
||||
|
||||
.action-row {
|
||||
height: var(--action-row-height);
|
||||
position: fixed;
|
||||
top: calc(var(--header-height) + var(--breadcrumbs-height));
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--quaternary-background);
|
||||
background-color: var(--secondary-background);
|
||||
}
|
||||
|
||||
.monaco-editor-tabs button[aria-expanded='true'],
|
||||
@@ -79,6 +86,7 @@
|
||||
width: 30%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.monaco-editor-tabs button + button {
|
||||
|
||||
@@ -32,10 +32,16 @@ textarea.inputarea {
|
||||
}
|
||||
|
||||
.breadcrumbs-demo {
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: var(--z-index-breadcrumbs);
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
height: var(--breadcrumbs-height);
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
|
||||
@media screen and (max-height: 300px) {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
.sp-preview-actions .sp-button:hover {
|
||||
background-color: var(--gray-10);
|
||||
background-color: var(--gray-10) !important;
|
||||
color: var(--gray-90) !important;
|
||||
border-color: var(--gray-90);
|
||||
border-color: var(--gray-90) !important;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface InteractiveFile {
|
||||
ext: string;
|
||||
name: string;
|
||||
contents: string;
|
||||
contentsHtml: string;
|
||||
fileKey?: string;
|
||||
}
|
||||
|
||||
@@ -52,7 +53,7 @@ const InteractiveEditor = ({ files }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='interactive-editor-wrapper' aria-hidden='true'>
|
||||
<div className='interactive-editor-wrapper'>
|
||||
<Sandpack
|
||||
template={
|
||||
got('tsx')
|
||||
|
||||
@@ -15,6 +15,7 @@ import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
import ChallengeDescription from '../components/challenge-description';
|
||||
import InteractiveEditor from '../components/interactive-editor';
|
||||
import ActionRow from '../classic/action-row';
|
||||
import Hotkeys from '../components/hotkeys';
|
||||
import ChallengeTitle from '../components/challenge-title';
|
||||
import VideoPlayer from '../components/video-player';
|
||||
@@ -70,12 +71,22 @@ interface ShowQuizProps {
|
||||
updateSolutionFormValues: () => void;
|
||||
}
|
||||
|
||||
function renderNodule(nodule: ChallengeNode['challenge']['nodules'][number]) {
|
||||
function renderNodule(
|
||||
nodule: ChallengeNode['challenge']['nodules'][number],
|
||||
showInteractiveEditor: boolean
|
||||
) {
|
||||
switch (nodule.type) {
|
||||
case 'paragraph':
|
||||
return <PrismFormatted text={nodule.data} />;
|
||||
case 'interactiveEditor':
|
||||
return <InteractiveEditor files={nodule.data} />;
|
||||
if (showInteractiveEditor) {
|
||||
return <InteractiveEditor files={nodule.data} />;
|
||||
} else {
|
||||
const files = nodule.data;
|
||||
return files.map((file, index) => (
|
||||
<PrismFormatted key={index} text={file.contentsHtml} />
|
||||
));
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -207,6 +218,20 @@ const ShowGeneric = ({
|
||||
|
||||
const sceneSubject = new SceneSubject();
|
||||
|
||||
// interactive editor
|
||||
const hasInteractiveEditor = nodules?.some(
|
||||
nodule => nodule.type === 'interactiveEditor'
|
||||
);
|
||||
|
||||
const [showInteractiveEditor, setShowInteractiveEditor] = useState(
|
||||
() => !!store.get('showInteractiveEditor')
|
||||
);
|
||||
|
||||
const toggleInteractiveEditor = () => {
|
||||
store.set('showInteractiveEditor', !showInteractiveEditor);
|
||||
setShowInteractiveEditor(!showInteractiveEditor);
|
||||
};
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
executeChallenge={handleSubmit}
|
||||
@@ -217,114 +242,129 @@ const ShowGeneric = ({
|
||||
<Helmet
|
||||
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
|
||||
/>
|
||||
<Container>
|
||||
<Row>
|
||||
<Spacer size='m' />
|
||||
<ChallengeTitle
|
||||
isCompleted={isChallengeCompleted}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
<Container
|
||||
fluid
|
||||
className={`generic-challenge-container${hasInteractiveEditor ? ' has-action-row' : ''}`}
|
||||
>
|
||||
{hasInteractiveEditor && (
|
||||
<ActionRow
|
||||
hasInteractiveEditor={hasInteractiveEditor}
|
||||
showInteractiveEditor={showInteractiveEditor}
|
||||
toggleInteractiveEditor={toggleInteractiveEditor}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacer size='m' />
|
||||
<Container>
|
||||
<Row>
|
||||
<Spacer size='m' />
|
||||
<ChallengeTitle
|
||||
isCompleted={isChallengeCompleted}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
|
||||
{description && (
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ChallengeDescription
|
||||
description={description}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
)}
|
||||
<Spacer size='m' />
|
||||
|
||||
{nodules?.map((nodule, i) => {
|
||||
return (
|
||||
<React.Fragment key={i}>{renderNodule(nodule)}</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
{videoId && (
|
||||
<>
|
||||
<VideoPlayer
|
||||
bilibiliIds={bilibiliIds}
|
||||
onVideoLoad={handleVideoIsLoaded}
|
||||
title={title}
|
||||
videoId={videoId}
|
||||
videoIsLoaded={videoIsLoaded}
|
||||
videoLocaleIds={videoLocaleIds}
|
||||
/>
|
||||
<Spacer size='m' />
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{scene && <Scene scene={scene} sceneSubject={sceneSubject} />}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
{transcript && <ChallengeTranscript transcript={transcript} />}
|
||||
|
||||
{instructions && (
|
||||
<>
|
||||
{description && (
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ChallengeDescription
|
||||
instructions={instructions}
|
||||
description={description}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<Spacer size='m' />
|
||||
</>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{assignments.length > 0 && (
|
||||
<ObserveKeys only={['ctrl', 'cmd', 'enter']}>
|
||||
<Assignments
|
||||
assignments={assignments}
|
||||
allAssignmentsCompleted={allAssignmentsCompleted}
|
||||
handleAssignmentChange={handleAssignmentChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
)}
|
||||
{nodules?.map((nodule, i) => {
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{renderNodule(nodule, showInteractiveEditor)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{questions.length > 0 && (
|
||||
<ObserveKeys only={['ctrl', 'cmd', 'enter']}>
|
||||
<MultipleChoiceQuestions
|
||||
questions={questions}
|
||||
selectedOptions={selectedMcqOptions}
|
||||
handleOptionChange={handleMcqOptionChange}
|
||||
submittedMcqAnswers={submittedMcqAnswers}
|
||||
showFeedback={showFeedback}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
)}
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
{videoId && (
|
||||
<>
|
||||
<VideoPlayer
|
||||
bilibiliIds={bilibiliIds}
|
||||
onVideoLoad={handleVideoIsLoaded}
|
||||
title={title}
|
||||
videoId={videoId}
|
||||
videoIsLoaded={videoIsLoaded}
|
||||
videoLocaleIds={videoLocaleIds}
|
||||
/>
|
||||
<Spacer size='m' />
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{explanation ? (
|
||||
<ChallengeExplanation explanation={explanation} />
|
||||
) : null}
|
||||
{scene && <Scene scene={scene} sceneSubject={sceneSubject} />}
|
||||
|
||||
{!hasAnsweredMcqCorrectly && (
|
||||
<p className='text-center'>{t('learn.answered-mcq')}</p>
|
||||
)}
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
{transcript && <ChallengeTranscript transcript={transcript} />}
|
||||
|
||||
<Button block={true} variant='primary' onClick={handleSubmit}>
|
||||
{questions.length == 0
|
||||
? t('buttons.submit')
|
||||
: t('buttons.check-answer')}
|
||||
</Button>
|
||||
<Spacer size='xxs' />
|
||||
<Button block={true} variant='primary' onClick={openHelpModal}>
|
||||
{t('buttons.ask-for-help')}
|
||||
</Button>
|
||||
{instructions && (
|
||||
<>
|
||||
<ChallengeDescription
|
||||
instructions={instructions}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<Spacer size='m' />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer size='l' />
|
||||
</Col>
|
||||
<CompletionModal />
|
||||
<HelpModal
|
||||
challengeTitle={title}
|
||||
challengeBlock={blockName}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
</Row>
|
||||
{assignments.length > 0 && (
|
||||
<ObserveKeys only={['ctrl', 'cmd', 'enter']}>
|
||||
<Assignments
|
||||
assignments={assignments}
|
||||
allAssignmentsCompleted={allAssignmentsCompleted}
|
||||
handleAssignmentChange={handleAssignmentChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
)}
|
||||
|
||||
{questions.length > 0 && (
|
||||
<ObserveKeys only={['ctrl', 'cmd', 'enter']}>
|
||||
<MultipleChoiceQuestions
|
||||
questions={questions}
|
||||
selectedOptions={selectedMcqOptions}
|
||||
handleOptionChange={handleMcqOptionChange}
|
||||
submittedMcqAnswers={submittedMcqAnswers}
|
||||
showFeedback={showFeedback}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
)}
|
||||
|
||||
{explanation ? (
|
||||
<ChallengeExplanation explanation={explanation} />
|
||||
) : null}
|
||||
|
||||
{!hasAnsweredMcqCorrectly && (
|
||||
<p className='text-center'>{t('learn.answered-mcq')}</p>
|
||||
)}
|
||||
|
||||
<Button block={true} variant='primary' onClick={handleSubmit}>
|
||||
{questions.length == 0
|
||||
? t('buttons.submit')
|
||||
: t('buttons.check-answer')}
|
||||
</Button>
|
||||
<Spacer size='xxs' />
|
||||
<Button block={true} variant='primary' onClick={openHelpModal}>
|
||||
{t('buttons.ask-for-help')}
|
||||
</Button>
|
||||
|
||||
<Spacer size='l' />
|
||||
</Col>
|
||||
<CompletionModal />
|
||||
<HelpModal
|
||||
challengeTitle={title}
|
||||
challengeBlock={blockName}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
</Container>
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
|
||||
@@ -192,7 +192,8 @@ const schema = Joi.object().keys({
|
||||
Joi.object().keys({
|
||||
ext: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
contents: Joi.string().required()
|
||||
contents: Joi.string().required(),
|
||||
contentsHtml: Joi.string().required()
|
||||
})
|
||||
),
|
||||
otherwise: Joi.string().required()
|
||||
|
||||
@@ -54,12 +54,16 @@ function getFiles(filesNodes) {
|
||||
|
||||
return filesNodes.map(node => {
|
||||
counts[node.lang] = counts[node.lang] ? counts[node.lang] + 1 : 1;
|
||||
|
||||
const contentsHtml = mdastToHTML([node]);
|
||||
|
||||
const out = {
|
||||
contents: node.value,
|
||||
ext: node.lang,
|
||||
name:
|
||||
getFilenames(node.lang) +
|
||||
(counts[node.lang] ? `-${counts[node.lang]}` : '')
|
||||
(counts[node.lang] ? `-${counts[node.lang]}` : ''),
|
||||
contentsHtml
|
||||
};
|
||||
|
||||
return out;
|
||||
|
||||
@@ -28,51 +28,51 @@ describe('add-interactive-editor plugin', () => {
|
||||
element => element.type === 'interactiveEditor'
|
||||
);
|
||||
|
||||
expect(editorElements).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
data: [
|
||||
{
|
||||
ext: expect.any(String),
|
||||
name: expect.any(String),
|
||||
contents: expect.stringContaining(
|
||||
'<div>This is an interactive element</div>'
|
||||
)
|
||||
}
|
||||
],
|
||||
type: 'interactiveEditor'
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
expect(editorElements).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
data: [
|
||||
{
|
||||
ext: expect.any(String),
|
||||
name: expect.any(String),
|
||||
contents: expect.stringContaining(
|
||||
'This is an interactive element'
|
||||
)
|
||||
}
|
||||
],
|
||||
type: 'interactiveEditor'
|
||||
},
|
||||
{
|
||||
data: [
|
||||
{
|
||||
ext: expect.any(String),
|
||||
name: expect.any(String),
|
||||
contents: expect.stringContaining(
|
||||
"console.log('Interactive JS');"
|
||||
)
|
||||
}
|
||||
],
|
||||
type: 'interactiveEditor'
|
||||
}
|
||||
])
|
||||
);
|
||||
expect(editorElements).toEqual([
|
||||
{
|
||||
type: 'interactiveEditor',
|
||||
data: [
|
||||
{
|
||||
contents: "console.log('Interactive JS');",
|
||||
ext: 'js',
|
||||
name: 'script-1',
|
||||
contentsHtml:
|
||||
'<pre><code class="language-js">console.log(\'Interactive JS\');\n</code></pre>'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'interactiveEditor',
|
||||
data: [
|
||||
{
|
||||
contents: '<div>This is an interactive element</div>',
|
||||
ext: 'html',
|
||||
name: 'index-1',
|
||||
contentsHtml:
|
||||
'<pre><code class="language-html"><div>This is an interactive element</div>\n</code></pre>'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'interactiveEditor',
|
||||
data: [
|
||||
{
|
||||
contents: '<div>This is an interactive element</div>',
|
||||
ext: 'html',
|
||||
name: 'index-1',
|
||||
contentsHtml:
|
||||
'<pre><code class="language-html"><div>This is an interactive element</div>\n</code></pre>'
|
||||
},
|
||||
{
|
||||
contents: "console.log('Interactive JS');",
|
||||
ext: 'js',
|
||||
name: 'script-1',
|
||||
contentsHtml:
|
||||
'<pre><code class="language-js">console.log(\'Interactive JS\');\n</code></pre>'
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('provides unique names for each file with the same extension', async () => {
|
||||
@@ -97,6 +97,9 @@ describe('add-interactive-editor plugin', () => {
|
||||
// Contents should match
|
||||
expect(files[0].contents).toBe("console.log('First JavaScript file');");
|
||||
expect(files[1].contents).toBe("console.log('Second JavaScript file');");
|
||||
|
||||
expect(files[0].contentsHtml).toContain('<pre><code class="language-js">');
|
||||
expect(files[1].contentsHtml).toContain('<pre><code class="language-js">');
|
||||
});
|
||||
|
||||
it('respects the order of elements in the original markdown', async () => {
|
||||
|
||||
Reference in New Issue
Block a user