Skip to content

Commit

Permalink
Editable code blocks (#118)
Browse files Browse the repository at this point in the history
* editable code blocks

* format, test

* test

* added catch

* maybe fix

* tutorial provider in tests

* format
  • Loading branch information
trean authored Oct 13, 2023
1 parent dd2baa4 commit f40f151
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 40 deletions.
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"react-dom": "^18.2.0",
"react-resizable-panels": "^0.0.51",
"react-router-dom": "^6.8.1",
"react-simple-code-editor": "^0.13.1",
"unist-util-visit": "^5.0.0",
"vite": "^4.3.3",
"vite-plugin-svgr": "^2.4.0",
Expand Down
91 changes: 60 additions & 31 deletions src/components/InteractiveTutorial/MdxComponents/CodeBlock.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Highlight, Prism, themes } from 'prism-react-renderer';
import Editor from 'react-simple-code-editor';
import { alpha, Box, Button } from '@mui/material';
import { requestFromCode } from '../../CodeEditorWindow/config/RequesFromCode';
import { useTutorial } from '../../../context/tutorial-context';
import { useTheme } from '@mui/material/styles';
import { styled, useTheme } from '@mui/material/styles';
import { PlayArrowOutlined } from '@mui/icons-material';
import { CopyButton } from '../../Common/CopyButton';
import { DARK_BACKGROUND, LIGHT_BACKGROUND } from './MdxComponents';

const StyledEditor = styled((props) => <Editor padding={0} textareaClassName={'code-block-textarea'} {...props} />)({
fontFamily: '"Menlo", monospace',
fontSize: '16px',
lineHeight: '24px',
fontWeight: '400',
'& .code-block-textarea': {
margin: '1rem 0 !important',
outline: 'none',
},
});

/**
* Run button for code block
* @param {string} code
Expand All @@ -18,13 +30,13 @@ import { DARK_BACKGROUND, LIGHT_BACKGROUND } from './MdxComponents';
export const RunButton = ({ code }) => {
const { setResult } = useTutorial();
const handleClick = () => {
requestFromCode(code, false).then((res) => {
if (res && res.status === 'ok') {
setResult(() => JSON.stringify(res));
} else {
requestFromCode(code, false)
.then((res) => {
setResult(() => JSON.stringify(res));
}
});
})
.catch((err) => {
setResult(() => JSON.stringify(err));
});
};
return (
<Button variant="outlined" endIcon={<PlayArrowOutlined />} onClick={handleClick} data-testid="code-block-run">
Expand All @@ -45,7 +57,7 @@ RunButton.propTypes = {
*/
export const CodeBlock = ({ children }) => {
const className = children.props.className || '';
const code = children.props.children.trim();
const [code, setCode] = useState(children.props.children.trim());
const language = className.replace(/language-/, '');
const withRunButton = children.props.withRunButton && JSON.parse(children.props.withRunButton);
const theme = useTheme();
Expand All @@ -59,6 +71,36 @@ export const CodeBlock = ({ children }) => {
(async () => await import('prismjs/components/prism-json'))();
}, []);

const handleChange = (code) => {
setCode(() => code);
};

const highlight = (code) => (
<Highlight code={code} language={language} theme={prismTheme} prism={Prism}>
{({ className, style, tokens, getLineProps, getTokenProps }) => {
return (
<pre
className={className}
style={{
wordBreak: 'keep-all',
whiteSpace: 'pre-wrap',
...style,
}}
data-testid={'code-block-pre'}
>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={token} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
);
}}
</Highlight>
);

return (
<Box
sx={{
Expand Down Expand Up @@ -86,28 +128,15 @@ export const CodeBlock = ({ children }) => {
<CopyButton text={code} />
</Box>
<Box sx={{ px: 2, pb: 1 }}>
<Highlight code={code} language={language} theme={prismTheme} prism={Prism}>
{({ className, style, tokens, getLineProps, getTokenProps }) => {
return (
<pre
className={className}
style={{
overflowX: 'auto',
...style,
}}
data-testid={'code-block-pre'}
>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={token} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
);
}}
</Highlight>
{withRunButton && (
<StyledEditor
value={code}
onValueChange={handleChange}
highlight={highlight}
data-testid={'code-block-editor'}
/>
)}
{!withRunButton && highlight(code)}
</Box>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, it, expect, afterEach, vi } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { CodeBlock, RunButton } from './CodeBlock';
import * as requestFromCodeMod from '../../CodeEditorWindow/config/RequesFromCode';
import { TutorialProvider } from '../../../context/tutorial-context';

const props = {
children: {
Expand All @@ -25,37 +26,64 @@ const requestFromCodeSpy = vi.spyOn(requestFromCodeMod, 'requestFromCode').mockI

describe('CodeBlock', () => {
it('should render RunButton with given code', () => {
render(<RunButton code={props.children.props.children} />);
render(
<TutorialProvider>
<RunButton code={props.children.props.children} />
</TutorialProvider>
);

expect(screen.getByTestId('code-block-run')).toBeInTheDocument();
expect(screen.getByText(/Run/)).toBeInTheDocument();
});

it('should call requestFromCode with given code', () => {
render(<RunButton code={props.children.props.children} />);
render(
<TutorialProvider>
<RunButton code={props.children.props.children} />
</TutorialProvider>
);
screen.getByTestId('code-block-run').click();

expect(requestFromCodeSpy).toHaveBeenCalledWith('{\n "name": "test"\n}', false);
});

it('should render CodeBlock with given code', () => {
render(<CodeBlock {...props} />);
render(
<TutorialProvider>
<CodeBlock {...props} />
</TutorialProvider>
);

expect(screen.getByTestId('code-block')).toBeInTheDocument();
expect(screen.getByTestId('code-block-pre')).toBeInTheDocument();
expect(screen.getByTestId('code-block-run')).toBeInTheDocument();
expect(screen.getByText(/{/)).toBeInTheDocument();
expect(screen.getByText(/"name": "test"/)).toBeInTheDocument();
expect(screen.getByText(/}/)).toBeInTheDocument();
expect(screen.getAllByText(/{/).length).toBe(2);
expect(screen.getAllByText(/"name": "test"/).length).toBe(2);
expect(screen.getAllByText(/}/).length).toBe(2);
expect(screen.getByText(/Run/)).toBeInTheDocument();
});

it('should render CodeBlock without run button', () => {
props.children.props.withRunButton = 'false';
render(<CodeBlock {...props} />);
const propsWithoutButton = structuredClone(props);
propsWithoutButton.children.props.withRunButton = 'false';

render(<CodeBlock {...propsWithoutButton} />);

expect(screen.getByTestId('code-block')).toBeInTheDocument();
expect(screen.getByTestId('code-block-pre')).toBeInTheDocument();
expect(screen.queryByTestId('code-block-run')).not.toBeInTheDocument();
});

it('should render an editor with given code if RunButton is present', () => {
render(
<TutorialProvider>
<CodeBlock {...props} />
</TutorialProvider>
);

expect(screen.queryByTestId('code-block-run')).toBeInTheDocument();
expect(screen.queryByTestId('code-block-editor')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue('{\n "name": "test"\n}');
});
});

0 comments on commit f40f151

Please sign in to comment.