Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 121 additions & 2 deletions client/modules/IDE/components/Preferences/Preferences.unit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { act, fireEvent, reduxRender, screen } from '../../../../test-utils';
import { initialState } from '../../reducers/preferences';
import Preferences from './index';
import * as PreferencesActions from '../../actions/preferences';
import * as FileActions from '../../actions/files';
import { initialState as filesInitialState } from '../../reducers/files';

describe('<Preferences />', () => {
// For backwards compatibility, spy on each action creator to see when it was dispatched.
Expand All @@ -13,13 +15,32 @@ describe('<Preferences />', () => {
})
);

const subject = (initialPreferences = {}) =>
const updateFileContentSpy = jest.spyOn(FileActions, 'updateFileContent');

const subject = (
initialPreferences = {},
sketchContent = `
function setup() {
createCanvas(400, 400);
}

function draw() {
background(220);
}
`
) =>
reduxRender(<Preferences />, {
initialState: {
preferences: {
...initialState,
...initialPreferences
}
},
files: filesInitialState().map((file) => ({
...file,
...(file.fileType === 'file' &&
file.name === 'sketch.js' &&
file.filePath === '' && { content: sketchContent })
}))
}
});

Expand Down Expand Up @@ -485,6 +506,104 @@ describe('<Preferences />', () => {
);
});
});
describe('loop protection toggle', () => {
it('is ON by default when no noprotect comment exists', () => {
subject({}, 'function setup() {}');

const onRadio = screen.getByRole('radio', {
name: /loop protection on/i
});

expect(onRadio.checked).toBe(true);
});

it('is OFF when noprotect comment exists', () => {
subject({}, '// noprotect\nfunction setup() {}');

const offRadio = screen.getByRole('radio', {
name: /loop protection off/i
});

expect(offRadio.checked).toBe(true);
});

it('adds noprotect comment when turning OFF', () => {
subject({}, 'function setup() {}');

const offRadio = screen.getByRole('radio', {
name: /loop protection off/i
});

act(() => {
fireEvent.click(offRadio);
});

expect(updateFileContentSpy).toHaveBeenCalledTimes(1);

const updatedSrc = updateFileContentSpy.mock.calls[0][1];
expect(updatedSrc).toMatch(/^\/\/ noprotect\b/);
expect(updatedSrc.match(/\/\/ noprotect/g)?.length).toBe(1);
});

it('removes noprotect comment when turning ON', () => {
subject({}, '// noprotect\nfunction setup() {}');

const onRadio = screen.getByRole('radio', {
name: /loop protection on/i
});

act(() => {
fireEvent.click(onRadio);
});

expect(updateFileContentSpy).toHaveBeenCalledTimes(1);

const updatedSrc = updateFileContentSpy.mock.calls[0][1];
expect(updatedSrc).not.toMatch(/\/\/\s*noprotect/);
});

it('does not dispatch when clicking already selected state (ON)', () => {
subject({}, 'function setup() {}');

const onRadio = screen.getByRole('radio', {
name: /loop protection on/i
});

act(() => {
fireEvent.click(onRadio);
});

expect(updateFileContentSpy).toHaveBeenCalledTimes(0);
});

it('does not duplicate noprotect comment when already OFF', () => {
subject({}, '// noprotect\nfunction setup() {}');

const offRadio = screen.getByRole('radio', {
name: /loop protection off/i
});

act(() => {
fireEvent.click(offRadio);
});

expect(updateFileContentSpy).toHaveBeenCalledTimes(0);
});
it('removes noprotect even if not at top', () => {
subject({}, 'function setup() {}\n// noprotect');

const onRadio = screen.getByRole('radio', {
name: /loop protection on/i
});

act(() => {
fireEvent.click(onRadio);
});

const updatedSrc = updateFileContentSpy.mock.calls[0][1];
expect(updatedSrc).not.toMatch(/noprotect/);
});
});
});

describe('can toggle between general settings and accessibility tabs successfully', () => {
Expand Down
57 changes: 57 additions & 0 deletions client/modules/IDE/components/Preferences/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { CmControllerContext } from '../../pages/IDEView';
import Stars from '../Stars';
import Admonition from '../Admonition';
import TextArea from '../TextArea';
import { hasNoProtect, toggleLoopProtection } from '../../utils/loopProtection';

export default function Preferences() {
const { t } = useTranslation();
Expand All @@ -53,6 +54,16 @@ export default function Preferences() {
const { versionInfo, indexID } = useP5Version();
const cmRef = useContext(CmControllerContext);
const [showStars, setShowStars] = useState(null);
const files = useSelector((s) => s.files);
const sketchFile = files.find(
Comment thread
skyash-dev marked this conversation as resolved.
Outdated
(file) =>
file.fileType === 'file' &&
file.name === 'sketch.js' &&
file.filePath === ''
);
const sketchSrc = sketchFile?.content;
const sketchID = sketchFile?.id;
const loopProtection = useMemo(() => !hasNoProtect(sketchSrc), [sketchSrc]);
const timerRef = useRef(null);
const pickerRef = useRef(null);
const onChangeVersion = (version) => {
Expand All @@ -65,6 +76,16 @@ export default function Preferences() {
}
};

function handleLoopProtection(enabled) {
if (!sketchID || !sketchSrc) return;

const next = toggleLoopProtection(sketchSrc, enabled);
if (next === sketchSrc) return;

dispatch(updateFileContent(sketchID, next));
cmRef.current?.updateFileContent(sketchID, next);
}

function onFontInputChange(event) {
const INTEGER_REGEX = /^[0-9\b]+$/;
if (event.target.value === '' || INTEGER_REGEX.test(event.target.value)) {
Expand Down Expand Up @@ -414,6 +435,42 @@ export default function Preferences() {
</label>
</fieldset>
</div>
<div className="preference">
<h4 className="preference__title">
{t('Preferences.LoopProtection')}
</h4>
<fieldset className="preference__options">
<input
type="radio"
onChange={() => handleLoopProtection(true)}
aria-label={t('Preferences.LoopProtectionOnARIA')}
name="loopprotection"
id="loopprotection-on"
className="preference__radio-button"
value="On"
checked={loopProtection}
/>
<label htmlFor="loopprotection-on" className="preference__option">
{t('Preferences.On')}
</label>
<input
type="radio"
onChange={() => handleLoopProtection(false)}
aria-label={t('Preferences.LoopProtectionOffARIA')}
name="loopprotection"
id="loopprotection-off"
className="preference__radio-button"
value="Off"
checked={!loopProtection}
/>
<label
htmlFor="loopprotection-off"
className="preference__option"
>
{t('Preferences.Off')}
</label>
</fieldset>
</div>
</TabPanel>
<TabPanel>
<div className="preference">
Expand Down
18 changes: 18 additions & 0 deletions client/modules/IDE/utils/loopProtection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const NO_PROTECT_REGEX = /^\s*\/\/\s*noprotect\b.*\n?/m;
Comment thread
skyash-dev marked this conversation as resolved.
Outdated

export function hasNoProtect(src = '') {
return NO_PROTECT_REGEX.test(src);
}

export function addNoProtect(src = '') {
if (hasNoProtect(src)) return src;
return `// noprotect\n${src}`;
}

export function removeNoProtect(src = '') {
return src.replace(NO_PROTECT_REGEX, '');
}

export function toggleLoopProtection(src = '', enabled) {
return enabled ? removeNoProtect(src) : addNoProtect(src);
}
1 change: 0 additions & 1 deletion client/styles/components/_overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
bottom: 0;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.5);
overflow-y: hidden;
Comment thread
skyash-dev marked this conversation as resolved.
}

.overlay__content {
Expand Down
3 changes: 3 additions & 0 deletions translations/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@
"WordWrap": "Word Wrap",
"WordWrapOnARIA": "wordwrap on",
"WordWrapOffARIA": "wordwrap off",
"LoopProtection": "Loop Protection",
"LoopProtectionOnARIA": "loop protection on",
"LoopProtectionOffARIA": "loop protection off",
"LineNumbers": "Line numbers",
"LineNumbersOnARIA": "line numbers on",
"LineNumbersOffARIA": "line numbers off",
Expand Down