Skip to content
22 changes: 0 additions & 22 deletions client/modules/IDE/components/Admonition.jsx

This file was deleted.

25 changes: 25 additions & 0 deletions client/modules/IDE/components/Admonition.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Admonition } from './Admonition';

describe('Admonition Component', () => {
it('renders with the correct title', () => {
render(<Admonition title="Important Note" />);
expect(screen.getByText('Important Note')).toBeInTheDocument();
});

it('renders the children content correctly', () => {
render(
<Admonition title="Warning">
<p>This is a warning message.</p>
</Admonition>
);
expect(screen.getByText('This is a warning message.')).toBeInTheDocument();
});

it('renders the title as a bold element', () => {
const { container } = render(<Admonition title="Bold Title" />);
const strongElement = container.querySelector('strong');
expect(strongElement).toHaveTextContent('Bold Title');
});
});
15 changes: 15 additions & 0 deletions client/modules/IDE/components/Admonition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';

interface AdmonitionProps {
children?: ReactNode;
title: string;
}

export const Admonition = ({ children, title }: AdmonitionProps) => (
<div className="admonition">
<p className="admonition__title">
<strong>{title}</strong>
</p>
{children}
</div>
);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import FloatingActionButton from './FloatingActionButton';
import { FloatingActionButton } from './FloatingActionButton';

export default {
title: 'IDE/FloatingActionButton',
Expand Down
41 changes: 41 additions & 0 deletions client/modules/IDE/components/FloatingActionButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { render, screen, fireEvent } from '../../../test-utils';
import { FloatingActionButton } from './FloatingActionButton';
import { startSketch, stopSketch } from '../actions/ide';

jest.mock('../actions/ide', () => ({
startSketch: jest.fn(() => ({ type: 'START_SKETCH' })),
stopSketch: jest.fn(() => ({ type: 'STOP_SKETCH' }))
}));

describe('FloatingActionButton', () => {
const defaultProps = {
syncFileContent: jest.fn(),
offsetBottom: 20
};

it('renders PlayIcon when not playing', () => {
render(<FloatingActionButton {...defaultProps} />, {
initialState: { ide: { isPlaying: false } }
});
// PlayIcon is rendered (SVG)
expect(screen.getByRole('button')).toBeInTheDocument();
});

it('calls syncFileContent and startSketch when clicked and not playing', () => {
render(<FloatingActionButton {...defaultProps} />, {
initialState: { ide: { isPlaying: false } }
});
fireEvent.click(screen.getByRole('button'));
expect(defaultProps.syncFileContent).toHaveBeenCalled();
expect(startSketch).toHaveBeenCalled();
});

it('calls stopSketch when clicked and playing', () => {
render(<FloatingActionButton {...defaultProps} />, {
initialState: { ide: { isPlaying: true } }
});
fireEvent.click(screen.getByRole('button'));
expect(stopSketch).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react';
import styled from 'styled-components';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import PlayIcon from '../../../images/triangle-arrow-right.svg';
import StopIcon from '../../../images/stop.svg';
import { prop, remSize } from '../../../theme';
import { startSketch, stopSketch } from '../actions/ide';
import { RootState } from '../../../reducers';

const Button = styled.button`
position: fixed;
Expand Down Expand Up @@ -38,8 +38,16 @@ const Button = styled.button`
}
`;

const FloatingActionButton = ({ syncFileContent, offsetBottom }) => {
const isPlaying = useSelector((state) => state.ide.isPlaying);
interface FloatingActionButtonProps {
syncFileContent: () => void;
offsetBottom: number;
}

export const FloatingActionButton = ({
syncFileContent,
offsetBottom
}: FloatingActionButtonProps) => {
const isPlaying = useSelector((state: RootState) => state.ide.isPlaying);
const dispatch = useDispatch();

return (
Expand All @@ -53,17 +61,16 @@ const FloatingActionButton = ({ syncFileContent, offsetBottom }) => {
if (!isPlaying) {
syncFileContent();
dispatch(startSketch());
} else dispatch(stopSketch());
} else {
dispatch(stopSketch());
}
}}
>
{isPlaying ? <StopIcon /> : <PlayIcon />}
{isPlaying ? (
<StopIcon focusable="false" aria-hidden="true" />
) : (
<PlayIcon focusable="false" aria-hidden="true" />
)}
</Button>
);
};

FloatingActionButton.propTypes = {
syncFileContent: PropTypes.func.isRequired,
offsetBottom: PropTypes.number.isRequired
};

export default FloatingActionButton;
3 changes: 1 addition & 2 deletions client/modules/IDE/components/Modal.stories.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import Modal from './Modal';
import { Modal } from './Modal';

export default {
title: 'IDE/Modal',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import React, { ReactNode } from 'react';
import { useModalClose } from '../../../common/useModalClose';
import ExitIcon from '../../../images/exit.svg';

// Common logic from NewFolderModal, NewFileModal, UploadFileModal
interface ModalProps {
title: string;
onClose: () => void;
closeAriaLabel: string;
contentClassName?: string;
children: ReactNode;
}

const Modal = ({
// Common logic from NewFolderModal, NewFileModal, UploadFileModal
export const Modal = ({
title,
onClose,
closeAriaLabel,
contentClassName,
contentClassName = '',
children
}) => {
const modalRef = useModalClose(onClose);
}: ModalProps) => {
const modalRef = useModalClose<HTMLElement>(onClose);

return (
<section className="modal" ref={modalRef}>
Expand All @@ -33,17 +39,3 @@ const Modal = ({
</section>
);
};

Modal.propTypes = {
title: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
closeAriaLabel: PropTypes.string.isRequired,
contentClassName: PropTypes.string,
children: PropTypes.node.isRequired
};

Modal.defaultProps = {
contentClassName: ''
};

export default Modal;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { fireEvent, render, screen } from '../../../test-utils';
import Modal from './Modal';
import { Modal } from './Modal';

describe('Modal', () => {
it('can render title', () => {
Expand Down
2 changes: 1 addition & 1 deletion client/modules/IDE/components/NewFileModal.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import Modal from './Modal';
import { Modal } from './Modal';
import NewFileForm from './NewFileForm';
import { closeNewFileModal } from '../actions/ide';

Expand Down
2 changes: 1 addition & 1 deletion client/modules/IDE/components/NewFolderModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { closeNewFolderModal } from '../actions/ide';
import Modal from './Modal';
import { Modal } from './Modal';
import NewFolderForm from './NewFolderForm';

const NewFolderModal = () => {
Expand Down
22 changes: 11 additions & 11 deletions client/modules/IDE/components/Preferences/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import VersionPicker from '../VersionPicker';
import { updateFileContent } from '../../actions/files';
import { CmControllerContext } from '../../pages/IDEView';
import Stars from '../Stars';
import Admonition from '../Admonition';
import TextArea from '../TextArea';
import { Admonition } from '../Admonition';
import { TextArea } from '../TextArea';

export default function Preferences() {
const { t } = useTranslation();
Expand Down Expand Up @@ -151,7 +151,7 @@ export default function Preferences() {
return (
<section className="preferences">
<Helmet>
<title>p5.js Web Editor | Preferences</title>
<title>{t('Preferences.TitleHelmet')}</title>
</Helmet>
<Tabs selectedIndex={tabIndex} onSelect={changeTab}>
<TabList>
Expand Down Expand Up @@ -584,7 +584,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5Sound(true));
}}
aria-label={`${t('Preferences.SoundAddon')} ${t(
'Preferences.AddonOn'
'Preferences.AddonOnARIA'
)}`}
name="soundaddon"
id="soundaddon-on"
Expand All @@ -601,7 +601,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5Sound(false));
}}
aria-label={`${t('Preferences.SoundAddon')} ${t(
'Preferences.AddonOff'
'Preferences.AddonOffARIA'
)}`}
name="soundaddon"
id="soundaddon-off"
Expand Down Expand Up @@ -641,7 +641,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5PreloadAddon(true))
}
aria-label={`${t('Preferences.PreloadAddon')} ${t(
'Preferences.AddonOn'
'Preferences.AddonOnARIA'
)}`}
name="preloadaddon"
id="preloadaddon-on"
Expand All @@ -661,7 +661,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5PreloadAddon(false))
}
aria-label={`${t('Preferences.PreloadAddon')} ${t(
'Preferences.AddonOff'
'Preferences.AddonOffARIA'
)}`}
name="preloadaddon"
id="preloadaddon-off"
Expand All @@ -688,7 +688,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5ShapesAddon(true))
}
aria-label={`${t('Preferences.ShapesAddon')} ${t(
'Preferences.AddonOn'
'Preferences.AddonOnARIA'
)}`}
name="shapesaddon"
id="shapesaddon-on"
Expand All @@ -708,7 +708,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5ShapesAddon(false))
}
aria-label={`${t('Preferences.ShapesAddon')} ${t(
'Preferences.AddonOff'
'Preferences.AddonOffARIA'
)}`}
name="shapesaddon"
id="shapesaddon-off"
Expand All @@ -735,7 +735,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5DataAddon(true))
}
aria-label={`${t('Preferences.DataAddon')} ${t(
'Preferences.AddonOn'
'Preferences.AddonOnARIA'
)}`}
name="dataaddon"
id="dataaddon-on"
Expand All @@ -752,7 +752,7 @@ export default function Preferences() {
updateHTML(versionInfo.setP5DataAddon(false))
}
aria-label={`${t('Preferences.DataAddon')} ${t(
'Preferences.AddonOff'
'Preferences.AddonOffARIA'
)}`}
name="dataaddon"
id="dataaddon-off"
Expand Down
38 changes: 38 additions & 0 deletions client/modules/IDE/components/TextArea.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '../../../test-utils';
import { TextArea } from './TextArea';
import { showToast } from '../actions/toast';

jest.mock('../actions/toast', () => ({
showToast: jest.fn(() => ({ type: 'SHOW_TOAST' }))
}));

describe('TextArea', () => {
const defaultProps = {
src: 'const x = 10;',
className: 'custom-class'
};

it('renders the source text', () => {
render(<TextArea {...defaultProps} />);
expect(screen.getByDisplayValue('const x = 10;')).toBeInTheDocument();
});

it('copies text to clipboard when copy button is clicked', async () => {
const mockClipboard = {
writeText: jest.fn().mockResolvedValue(undefined)
};
Object.assign(navigator, {
clipboard: mockClipboard
});

render(<TextArea {...defaultProps} />);
const copyButton = screen.getByRole('button');
fireEvent.click(copyButton);

expect(mockClipboard.writeText).toHaveBeenCalledWith('const x = 10;');
await waitFor(() => {
expect(showToast).toHaveBeenCalled();
});
});
});
Loading